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