1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *   http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.google.android.apps.common.testing.ui.espresso.action;
18
19import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.isDisplayingAtLeast;
20import static com.google.common.base.Preconditions.checkArgument;
21
22import com.google.common.base.Optional;
23import com.google.common.collect.Lists;
24import com.google.common.collect.Range;
25
26import android.os.Build;
27import android.view.View;
28import android.widget.AbsListView;
29import android.widget.Adapter;
30import android.widget.AdapterView;
31import android.widget.AdapterViewAnimator;
32import android.widget.AdapterViewFlipper;
33
34import java.util.List;
35
36/**
37 * Implementations of {@link AdapterViewProtocol} for standard SDK Widgets.
38 *
39 */
40public final class AdapterViewProtocols {
41
42  /**
43   * Consider views which have over this percentage of their area visible to the user
44   * to be fully rendered.
45   */
46  private static final int FULLY_RENDERED_PERCENTAGE_CUTOFF = 90;
47
48  private AdapterViewProtocols() {}
49
50  private static final AdapterViewProtocol STANDARD_PROTOCOL = new StandardAdapterViewProtocol();
51
52  /**
53   * Creates an implementation of AdapterViewProtocol that can work with AdapterViews that do not
54   * break method contracts on AdapterView.
55   *
56   */
57  public static AdapterViewProtocol standardProtocol() {
58    return STANDARD_PROTOCOL;
59  }
60
61  // TODO(user): expandablelistview protocols
62
63  private static final class StandardAdapterViewProtocol implements AdapterViewProtocol {
64    @Override
65    public Iterable<AdaptedData> getDataInAdapterView(AdapterView<? extends Adapter> adapterView) {
66      List<AdaptedData> datas = Lists.newArrayList();
67      for (int i = 0; i < adapterView.getCount(); i++) {
68        datas.add(
69            new AdaptedData.Builder()
70              .withData(adapterView.getItemAtPosition(i))
71              .withOpaqueToken(i)
72              .build());
73      }
74      return datas;
75    }
76
77    @Override
78    public Optional<AdaptedData> getDataRenderedByView(AdapterView<? extends Adapter> adapterView,
79        View descendantView) {
80      if (adapterView == descendantView.getParent()) {
81        int position = adapterView.getPositionForView(descendantView);
82        if (position != AdapterView.INVALID_POSITION) {
83          return Optional.of(new AdaptedData.Builder()
84              .withData(adapterView.getItemAtPosition(position))
85              .withOpaqueToken(Integer.valueOf(position))
86              .build());
87        }
88      }
89      return Optional.absent();
90    }
91
92    @Override
93    public void makeDataRenderedWithinAdapterView(
94        AdapterView<? extends Adapter> adapterView, AdaptedData data) {
95      checkArgument(data.opaqueToken instanceof Integer, "Not my data: %s", data);
96      int position = ((Integer) data.opaqueToken).intValue();
97
98      boolean moved = false;
99      // set selection should always work, we can give a little better experience if per subtype
100      // though.
101      if (Build.VERSION.SDK_INT > 7) {
102        if (adapterView instanceof AbsListView) {
103          if (Build.VERSION.SDK_INT > 10) {
104            ((AbsListView) adapterView).smoothScrollToPositionFromTop(position,
105                adapterView.getPaddingTop(), 0);
106          } else {
107            ((AbsListView) adapterView).smoothScrollToPosition(position);
108          }
109          moved = true;
110        }
111        if (Build.VERSION.SDK_INT > 10) {
112          if (adapterView instanceof AdapterViewAnimator) {
113            if (adapterView instanceof AdapterViewFlipper) {
114              ((AdapterViewFlipper) adapterView).stopFlipping();
115            }
116            ((AdapterViewAnimator) adapterView).setDisplayedChild(position);
117            moved = true;
118          }
119        }
120      }
121      if (!moved) {
122        adapterView.setSelection(position);
123      }
124    }
125
126    @SuppressWarnings("deprecation")
127    @Override
128    public boolean isDataRenderedWithinAdapterView(
129        AdapterView<? extends Adapter> adapterView, AdaptedData adaptedData) {
130      checkArgument(adaptedData.opaqueToken instanceof Integer, "Not my data: %s", adaptedData);
131      int dataPosition = ((Integer) adaptedData.opaqueToken).intValue();
132
133      if (Range.closed(adapterView.getFirstVisiblePosition(), adapterView.getLastVisiblePosition())
134          .contains(dataPosition)) {
135        if (adapterView.getFirstVisiblePosition() == adapterView.getLastVisiblePosition()) {
136          // thats a huge element.
137          return true;
138        } else {
139          return isElementFullyRendered(adapterView,
140              dataPosition - adapterView.getFirstVisiblePosition());
141        }
142      } else {
143        return false;
144      }
145    }
146
147    private boolean isElementFullyRendered(AdapterView<? extends Adapter> adapterView,
148        int childAt) {
149      View element = adapterView.getChildAt(childAt);
150      // Occassionally we'll have to fight with smooth scrolling logic on our definition of when
151      // there is extra scrolling to be done. In particular if the element is the first or last
152      // element of the list, the smooth scroller may decide that no work needs to be done to scroll
153      // to the element if a certain percentage of it is on screen. Ugh. Sigh. Yuck.
154
155      return isDisplayingAtLeast(FULLY_RENDERED_PERCENTAGE_CUTOFF).matches(element);
156    }
157  }
158}
159