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