1 package com.xtremelabs.robolectric.shadows; 2 3 import android.database.DataSetObserver; 4 import android.os.Handler; 5 import android.view.View; 6 import android.widget.Adapter; 7 import android.widget.AdapterView; 8 import com.xtremelabs.robolectric.internal.Implementation; 9 import com.xtremelabs.robolectric.internal.Implements; 10 import com.xtremelabs.robolectric.internal.RealObject; 11 12 import java.util.ArrayList; 13 import java.util.List; 14 15 import static com.xtremelabs.robolectric.Robolectric.shadowOf; 16 17 @SuppressWarnings({"UnusedDeclaration"}) 18 @Implements(AdapterView.class) 19 public class ShadowAdapterView extends ShadowViewGroup { 20 private static int ignoreRowsAtEndOfList = 0; 21 private static boolean automaticallyUpdateRowViews = true; 22 23 @RealObject 24 private AdapterView realAdapterView; 25 26 private Adapter adapter; 27 private View mEmptyView; 28 private AdapterView.OnItemSelectedListener onItemSelectedListener; 29 private AdapterView.OnItemClickListener onItemClickListener; 30 private AdapterView.OnItemLongClickListener onItemLongClickListener; 31 private boolean valid = false; 32 private int selectedPosition; 33 private int itemCount = 0; 34 35 private List<Object> previousItems = new ArrayList<Object>(); 36 37 @Implementation setAdapter(Adapter adapter)38 public void setAdapter(Adapter adapter) { 39 this.adapter = adapter; 40 41 if (null != adapter) { 42 adapter.registerDataSetObserver(new AdapterViewDataSetObserver()); 43 } 44 45 invalidateAndScheduleUpdate(); 46 setSelection(0); 47 } 48 49 @Implementation setEmptyView(View emptyView)50 public void setEmptyView(View emptyView) { 51 this.mEmptyView = emptyView; 52 updateEmptyStatus(adapter == null || adapter.isEmpty()); 53 } 54 55 @Implementation getPositionForView(android.view.View view)56 public int getPositionForView(android.view.View view) { 57 while (view.getParent() != null && view.getParent() != realView) { 58 view = (View) view.getParent(); 59 } 60 61 for (int i = 0; i < getChildCount(); i++) { 62 if (view == getChildAt(i)) { 63 return i; 64 } 65 } 66 67 return AdapterView.INVALID_POSITION; 68 } 69 invalidateAndScheduleUpdate()70 private void invalidateAndScheduleUpdate() { 71 valid = false; 72 itemCount = adapter == null ? 0 : adapter.getCount(); 73 if (mEmptyView != null) { 74 updateEmptyStatus(itemCount == 0); 75 } 76 77 if (hasOnItemSelectedListener() && itemCount == 0) { 78 onItemSelectedListener.onNothingSelected(realAdapterView); 79 } 80 81 new Handler().post(new Runnable() { 82 @Override 83 public void run() { 84 if (!valid) { 85 update(); 86 valid = true; 87 } 88 } 89 }); 90 } 91 hasOnItemSelectedListener()92 private boolean hasOnItemSelectedListener() { 93 return onItemSelectedListener != null; 94 } 95 updateEmptyStatus(boolean empty)96 private void updateEmptyStatus(boolean empty) { 97 // code taken from the real AdapterView and commented out where not (yet?) applicable 98 99 // we don't deal with filterMode yet... 100 // if (isInFilterMode()) { 101 // empty = false; 102 // } 103 104 if (empty) { 105 if (mEmptyView != null) { 106 mEmptyView.setVisibility(View.VISIBLE); 107 setVisibility(View.GONE); 108 } else { 109 // If the caller just removed our empty view, make sure the list view is visible 110 setVisibility(View.VISIBLE); 111 } 112 113 // leave layout for the moment... 114 // // We are now GONE, so pending layouts will not be dispatched. 115 // // Force one here to make sure that the state of the list matches 116 // // the state of the adapter. 117 // if (mDataChanged) { 118 // this.onLayout(false, mLeft, mTop, mRight, mBottom); 119 // } 120 } else { 121 if (mEmptyView != null) { 122 mEmptyView.setVisibility(View.GONE); 123 } 124 setVisibility(View.VISIBLE); 125 } 126 } 127 128 /** 129 * Check if our adapter's items have changed without {@code onChanged()} or {@code onInvalidated()} having been called. 130 * 131 * @return true if the object is valid, false if not 132 * @throws RuntimeException if the items have been changed without notification 133 */ checkValidity()134 public boolean checkValidity() { 135 update(); 136 return valid; 137 } 138 139 /** 140 * Set to avoid calling getView() on the last row(s) during validation. Useful if you are using a special 141 * last row, e.g. one that goes and fetches more list data as soon as it comes into view. This sets a static 142 * on the class, so be sure to call it again and set it back to 0 at the end of your test. 143 * 144 * @param countOfRows The number of rows to ignore at the end of the list. 145 * @see com.xtremelabs.robolectric.shadows.ShadowAdapterView#checkValidity() 146 */ ignoreRowsAtEndOfListDuringValidation(int countOfRows)147 public static void ignoreRowsAtEndOfListDuringValidation(int countOfRows) { 148 ignoreRowsAtEndOfList = countOfRows; 149 } 150 151 /** 152 * Use this static method to turn off the feature of this class which calls getView() on all of the 153 * adapter's rows in setAdapter() and after notifyDataSetChanged() or notifyDataSetInvalidated() is 154 * called on the adapter. This feature is turned on by default. This sets a static on the class, so 155 * set it back to true at the end of your test to avoid test pollution. 156 * 157 * @param shouldUpdate false to turn off the feature, true to turn it back on 158 */ automaticallyUpdateRowViews(boolean shouldUpdate)159 public static void automaticallyUpdateRowViews(boolean shouldUpdate) { 160 automaticallyUpdateRowViews = shouldUpdate; 161 } 162 163 @Implementation getSelectedItemPosition()164 public int getSelectedItemPosition() { 165 return selectedPosition; 166 } 167 168 @Implementation getSelectedItem()169 public Object getSelectedItem() { 170 int pos = getSelectedItemPosition(); 171 return getItemAtPosition(pos); 172 } 173 174 @Implementation getAdapter()175 public Adapter getAdapter() { 176 return adapter; 177 } 178 179 @Implementation getCount()180 public int getCount() { 181 return itemCount; 182 } 183 184 @Implementation setOnItemSelectedListener(AdapterView.OnItemSelectedListener listener)185 public void setOnItemSelectedListener(AdapterView.OnItemSelectedListener listener) { 186 this.onItemSelectedListener = listener; 187 } 188 189 @Implementation getOnItemSelectedListener()190 public final AdapterView.OnItemSelectedListener getOnItemSelectedListener() { 191 return onItemSelectedListener; 192 } 193 194 @Implementation setOnItemClickListener(AdapterView.OnItemClickListener listener)195 public void setOnItemClickListener(AdapterView.OnItemClickListener listener) { 196 this.onItemClickListener = listener; 197 } 198 199 @Implementation getOnItemClickListener()200 public final AdapterView.OnItemClickListener getOnItemClickListener() { 201 return onItemClickListener; 202 } 203 204 @Implementation setOnItemLongClickListener(AdapterView.OnItemLongClickListener listener)205 public void setOnItemLongClickListener(AdapterView.OnItemLongClickListener listener) { 206 this.onItemLongClickListener = listener; 207 } 208 209 @Implementation getOnItemLongClickListener()210 public AdapterView.OnItemLongClickListener getOnItemLongClickListener() { 211 return onItemLongClickListener; 212 } 213 214 @Implementation getItemAtPosition(int position)215 public Object getItemAtPosition(int position) { 216 Adapter adapter = getAdapter(); 217 return (adapter == null || position < 0) ? null : adapter.getItem(position); 218 } 219 220 @Implementation getItemIdAtPosition(int position)221 public long getItemIdAtPosition(int position) { 222 Adapter adapter = getAdapter(); 223 return (adapter == null || position < 0) ? AdapterView.INVALID_ROW_ID : adapter.getItemId(position); 224 } 225 226 @Implementation setSelection(final int position)227 public void setSelection(final int position) { 228 selectedPosition = position; 229 230 if (selectedPosition >= 0) { 231 new Handler().post(new Runnable() { 232 @Override 233 public void run() { 234 if (hasOnItemSelectedListener()) { 235 onItemSelectedListener.onItemSelected(realAdapterView, getChildAt(position), position, getAdapter().getItemId(position)); 236 } 237 } 238 }); 239 } 240 } 241 242 @Implementation performItemClick(View view, int position, long id)243 public boolean performItemClick(View view, int position, long id) { 244 if (onItemClickListener != null) { 245 onItemClickListener.onItemClick(realAdapterView, view, position, id); 246 return true; 247 } 248 return false; 249 } 250 performItemLongClick(View view, int position, long id)251 public boolean performItemLongClick(View view, int position, long id) { 252 if (onItemLongClickListener != null) { 253 onItemLongClickListener.onItemLongClick(realAdapterView, view, position, id); 254 return true; 255 } 256 return false; 257 } 258 performItemClick(int position)259 public boolean performItemClick(int position) { 260 return realAdapterView.performItemClick(realAdapterView.getChildAt(position), 261 position, realAdapterView.getItemIdAtPosition(position)); 262 } 263 findIndexOfItemContainingText(String targetText)264 public int findIndexOfItemContainingText(String targetText) { 265 for (int i = 0; i < realAdapterView.getChildCount(); i++) { 266 View childView = realAdapterView.getChildAt(i); 267 String innerText = shadowOf(childView).innerText(); 268 if (innerText.contains(targetText)) { 269 return i; 270 } 271 } 272 return -1; 273 } 274 findItemContainingText(String targetText)275 public View findItemContainingText(String targetText) { 276 int itemIndex = findIndexOfItemContainingText(targetText); 277 if (itemIndex == -1) { 278 return null; 279 } 280 return realAdapterView.getChildAt(itemIndex); 281 } 282 clickFirstItemContainingText(String targetText)283 public void clickFirstItemContainingText(String targetText) { 284 int itemIndex = findIndexOfItemContainingText(targetText); 285 if (itemIndex == -1) { 286 throw new IllegalArgumentException("No item found containing text \"" + targetText + "\""); 287 } 288 performItemClick(itemIndex); 289 } 290 291 @Implementation getEmptyView()292 public View getEmptyView() { 293 return mEmptyView; 294 } 295 update()296 private void update() { 297 if (!automaticallyUpdateRowViews) { 298 return; 299 } 300 301 super.removeAllViews(); 302 addViews(); 303 } 304 addViews()305 protected void addViews() { 306 Adapter adapter = getAdapter(); 307 if (adapter != null) { 308 if (valid && (previousItems.size() - ignoreRowsAtEndOfList != adapter.getCount() - ignoreRowsAtEndOfList)) { 309 throw new ArrayIndexOutOfBoundsException("view is valid but adapter.getCount() has changed from " + previousItems.size() + " to " + adapter.getCount()); 310 } 311 312 List<Object> newItems = new ArrayList<Object>(); 313 for (int i = 0; i < adapter.getCount() - ignoreRowsAtEndOfList; i++) { 314 View view = adapter.getView(i, null, realAdapterView); 315 // don't add null views 316 if (view != null) { 317 addView(view); 318 } 319 newItems.add(adapter.getItem(i)); 320 } 321 322 if (valid && !newItems.equals(previousItems)) { 323 throw new RuntimeException("view is valid but current items <" + newItems + "> don't match previous items <" + previousItems + ">"); 324 } 325 previousItems = newItems; 326 } 327 } 328 329 /** 330 * Simple default implementation of {@code android.database.DataSetObserver} 331 */ 332 protected class AdapterViewDataSetObserver extends DataSetObserver { 333 @Override onChanged()334 public void onChanged() { 335 invalidateAndScheduleUpdate(); 336 } 337 338 @Override onInvalidated()339 public void onInvalidated() { 340 invalidateAndScheduleUpdate(); 341 } 342 } 343 } 344