AdapterViewProtocols.java
/*
* Copyright (C) 2014 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 androidx.test.espresso.action;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayingAtLeast;
import static com.google.common.base.Preconditions.checkArgument;
import android.database.Cursor;
import android.os.Build;
import android.util.Log;
import android.view.View;
import android.widget.AbsListView;
import android.widget.Adapter;
import android.widget.AdapterView;
import android.widget.AdapterViewAnimator;
import android.widget.AdapterViewFlipper;
import androidx.test.espresso.util.EspressoOptional;
import com.google.common.collect.Lists;
import com.google.common.collect.Range;
import java.util.List;
/** Implementations of {@link AdapterViewProtocol} for standard SDK Widgets. */
public final class AdapterViewProtocols {
/**
* Consider views which have over this percentage of their area visible to the user to be fully
* rendered.
*/
private static final int FULLY_RENDERED_PERCENTAGE_CUTOFF = 90;
private AdapterViewProtocols() {}
private static final AdapterViewProtocol STANDARD_PROTOCOL = new StandardAdapterViewProtocol();
/**
* Creates an implementation of AdapterViewProtocol that can work with AdapterViews that do not
* break method contracts on AdapterView.
*/
public static AdapterViewProtocol standardProtocol() {
return STANDARD_PROTOCOL;
}
private static final class StandardAdapterViewProtocol implements AdapterViewProtocol {
private static final String TAG = "StdAdapterViewProtocol";
/** Required for Espresso remote serialization, called reflectively. */
public StandardAdapterViewProtocol() {}
private static final class StandardDataFunction implements DataFunction {
private final Object dataAtPosition;
private final int position;
private StandardDataFunction(Object dataAtPosition, int position) {
checkArgument(position >= 0, "position must be >= 0");
this.dataAtPosition = dataAtPosition;
this.position = position;
}
@Override
public Object getData() {
if (dataAtPosition instanceof Cursor) {
if (!((Cursor) dataAtPosition).moveToPosition(position)) {
Log.e(TAG, "Cannot move cursor to position: " + position);
}
}
return dataAtPosition;
}
}
@Override
public Iterable<AdaptedData> getDataInAdapterView(AdapterView<? extends Adapter> adapterView) {
List<AdaptedData> datas = Lists.newArrayList();
for (int i = 0; i < adapterView.getCount(); i++) {
int position = i;
Object dataAtPosition = adapterView.getItemAtPosition(position);
datas.add(
new AdaptedData.Builder()
.withDataFunction(new StandardDataFunction(dataAtPosition, position))
.withOpaqueToken(position)
.build());
}
return datas;
}
@Override
public EspressoOptional<AdaptedData> getDataRenderedByView(
AdapterView<? extends Adapter> adapterView, View descendantView) {
if (adapterView == descendantView.getParent()) {
int position = adapterView.getPositionForView(descendantView);
if (position != AdapterView.INVALID_POSITION) {
return EspressoOptional.of(
new AdaptedData.Builder()
.withDataFunction(
new StandardDataFunction(adapterView.getItemAtPosition(position), position))
.withOpaqueToken(Integer.valueOf(position))
.build());
}
}
return EspressoOptional.absent();
}
@Override
public void makeDataRenderedWithinAdapterView(
AdapterView<? extends Adapter> adapterView, AdaptedData data) {
checkArgument(data.opaqueToken instanceof Integer, "Not my data: %s", data);
int position = ((Integer) data.opaqueToken).intValue();
boolean moved = false;
// set selection should always work, we can give a little better experience if per subtype
// though.
if (Build.VERSION.SDK_INT > 7) {
if (adapterView instanceof AbsListView) {
if (Build.VERSION.SDK_INT > 10) {
((AbsListView) adapterView)
.smoothScrollToPositionFromTop(position, adapterView.getPaddingTop(), 0);
} else {
((AbsListView) adapterView).smoothScrollToPosition(position);
}
moved = true;
}
if (Build.VERSION.SDK_INT > 10) {
if (adapterView instanceof AdapterViewAnimator) {
if (adapterView instanceof AdapterViewFlipper) {
((AdapterViewFlipper) adapterView).stopFlipping();
}
((AdapterViewAnimator) adapterView).setDisplayedChild(position);
moved = true;
}
}
}
if (!moved) {
adapterView.setSelection(position);
}
}
@Override
public boolean isDataRenderedWithinAdapterView(
AdapterView<? extends Adapter> adapterView, AdaptedData adaptedData) {
checkArgument(adaptedData.opaqueToken instanceof Integer, "Not my data: %s", adaptedData);
int dataPosition = ((Integer) adaptedData.opaqueToken).intValue();
boolean inView = false;
if (Range.closed(adapterView.getFirstVisiblePosition(), adapterView.getLastVisiblePosition())
.contains(dataPosition)) {
if (adapterView.getFirstVisiblePosition() == adapterView.getLastVisiblePosition()) {
// thats a huge element.
inView = true;
} else {
inView =
isElementFullyRendered(
adapterView, dataPosition - adapterView.getFirstVisiblePosition());
}
}
if (inView) {
// stops animations - locks in our x/y location.
adapterView.setSelection(dataPosition);
}
return inView;
}
private boolean isElementFullyRendered(
AdapterView<? extends Adapter> adapterView, int childAt) {
View element = adapterView.getChildAt(childAt);
// Occassionally we'll have to fight with smooth scrolling logic on our definition of when
// there is extra scrolling to be done. In particular if the element is the first or last
// element of the list, the smooth scroller may decide that no work needs to be done to scroll
// to the element if a certain percentage of it is on screen. Ugh. Sigh. Yuck.
return isDisplayingAtLeast(FULLY_RENDERED_PERCENTAGE_CUTOFF).matches(element);
}
}
}