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.common.base.Preconditions.checkNotNull;
20import com.google.common.base.Optional;
21
22import android.view.View;
23import android.widget.Adapter;
24import android.widget.AdapterView;
25
26import javax.annotation.Nullable;
27
28/**
29 * A sadly necessary layer of indirection to interact with AdapterViews.
30 * <p>
31 * Generally any subclass should respect the contracts and behaviors of its superclass. Otherwise
32 * it becomes impossible to work generically with objects that all claim to share a supertype - you
33 * need special cases to perform the same operation 'owned' by the supertype for each sub-type. The
34 * 'is - a' relationship is broken.
35 * </p>
36 *
37 * <p>
38 * Android breaks the Liskov substitution principal with ExpandableListView - you can't use
39 * getAdapter(), getItemAtPosition(), and other methods common to AdapterViews on an
40 * ExpandableListView because an ExpandableListView isn't an adapterView - they just share a lot of
41 * code.
42 * </p>
43 *
44 * <p>
45 * This interface exists to work around this wart (which sadly is copied in other projects too) and
46 * lets the implementor translate Espresso's needs and manipulations of the AdapterView into calls
47 * that make sense for the given subtype and context.
48 * </p>
49 *
50 * <p><i>
51 * If you have to implement this to talk to widgets your own project defines - I'm sorry.
52 * </i><p>
53 *
54 */
55public interface AdapterViewProtocol {
56
57  /**
58   * Returns all data this AdapterViewProtocol can find within the given AdapterView.
59   *
60   * <p>
61   * Any AdaptedData returned by this method can be passed to makeDataRenderedWithinView and the
62   * implementation should make the AdapterView bring that data item onto the screen.
63   * </p>
64   *
65   * @param adapterView the AdapterView we want to interrogate the contents of.
66   * @return an {@link Iterable} of AdaptedDatas representing all data the implementation sees in
67   *         this view
68   * @throws IllegalArgumentException if the implementation doesn't know how to manipulate the given
69   *         adapter view.
70   */
71  Iterable<AdaptedData> getDataInAdapterView(AdapterView<? extends Adapter> adapterView);
72
73  /**
74   * Returns the data object this particular view is rendering if possible.
75   *
76   * <p>
77   * Implementations are expected to create a relationship between the data in the AdapterView and
78   * the descendant views of the AdapterView that obeys the following conditions:
79   * </p>
80   *
81   * <ul>
82   * <li>For each descendant view there exists either 0 or 1 data objects it is rendering.</li>
83   * <li>For each data object the AdapterView there exists either 0 or 1 descendant views which
84   *   claim to be rendering it.</li>
85   * </ul>
86   *
87   * <p> For example - if a PersonObject is rendered into: </p>
88   * <code>
89   * LinearLayout
90   *   ImageView picture
91   *   TextView firstName
92   *   TextView lastName
93   * </code>
94   *
95   * <p>
96   * It would be expected that getDataRenderedByView(adapter, LinearLayout) would return the
97   * PersonObject. If it were called instead with the TextView or ImageView it would return
98   * Object.absent().
99   * </p>
100   *
101   * @param adapterView the adapterview hosting the data.
102   * @param descendantView a view which is a child, grand-child, or deeper descendant of adapterView
103   * @return an optional data object the descendant view is rendering.
104   * @throws IllegalArgumentException if this protocol cannot interrogate this class of adapterView
105   */
106  Optional<AdaptedData> getDataRenderedByView(
107      AdapterView<? extends Adapter> adapterView, View descendantView);
108
109  /**
110   * Requests that a particular piece of data held in this AdapterView is actually rendered by it.
111   *
112   * <p>
113   * After calling this method it expected that there will exist some descendant view of adapterView
114   * for which calling getDataRenderedByView(adapterView, descView).get() == data.data is true.
115   * <p>
116   *
117   * </p>
118   * Note: this need not happen immediately. EG: an implementor handling ListView may call
119   * listView.smoothScrollToPosition(data.opaqueToken) - which kicks off an animated scroll over
120   * the list to the given position. The animation may be in progress after this call returns. The
121   * only guarantee is that eventually - with no further interaction necessary - this data item
122   * will be rendered as a child or deeper descendant of this AdapterView.
123   * </p>
124   *
125   * @param adapterView the adapterView hosting the data.
126   * @param data an AdaptedData instance retrieved by a prior call to getDataInAdapterView
127   * @throws IllegalArgumentException if this protocol cannot manipulate adapterView or if data is
128   *   not owned by this AdapterViewProtocol.
129   */
130  void makeDataRenderedWithinAdapterView(
131      AdapterView<? extends Adapter> adapterView, AdaptedData data);
132
133
134  /**
135   * Indicates whether or not there now exists a descendant view within adapterView that
136   * is rendering this data.
137   *
138   * @param adapterView the AdapterView hosting this data.
139   * @param adaptedData the data we are checking the display state for.
140   * @return true if the data is rendered by a view in the adapterView, false otherwise.
141   */
142  boolean isDataRenderedWithinAdapterView(
143      AdapterView<? extends Adapter> adapterView, AdaptedData adaptedData);
144
145
146  /**
147   * A holder that associates a data object from an AdapterView with a token the
148   * AdapterViewProtocol can use to force that data object to be rendered as a child or deeper
149   * descendant of the adapter view.
150   */
151  public static class AdaptedData {
152
153    /**
154     * One of the objects the AdapterView is exposing to the user.
155     */
156    @Nullable
157    public final Object data;
158
159    /**
160     * A token the implementor of AdapterViewProtocol can use to force the adapterView to display
161     * this data object as a child or deeper descendant in it. Equal opaqueToken point to the same
162     * data object on the AdapterView.
163     */
164    public final Object opaqueToken;
165
166    @Override
167    public String toString() {
168      return String.format("Data: %s (class: %s) token: %s", data,
169          null == data ? null : data.getClass(), opaqueToken);
170    }
171
172    private AdaptedData(Object data, Object opaqueToken) {
173      this.data = data;
174      this.opaqueToken = checkNotNull(opaqueToken);
175    }
176
177    public static class Builder {
178      private Object data;
179      private Object opaqueToken;
180
181      public Builder withData(@Nullable Object data) {
182        this.data = data;
183        return this;
184      }
185
186      public Builder withOpaqueToken(@Nullable Object opaqueToken) {
187        this.opaqueToken = opaqueToken;
188        return this;
189      }
190
191      public AdaptedData build() {
192        return new AdaptedData(data, opaqueToken);
193      }
194    }
195  }
196}
197