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