1package com.xtremelabs.robolectric.shadows;
2
3import android.database.DataSetObserver;
4import android.os.Handler;
5import android.view.View;
6import android.widget.Adapter;
7import android.widget.AdapterView;
8import com.xtremelabs.robolectric.internal.Implementation;
9import com.xtremelabs.robolectric.internal.Implements;
10import com.xtremelabs.robolectric.internal.RealObject;
11
12import java.util.ArrayList;
13import java.util.List;
14
15import static com.xtremelabs.robolectric.Robolectric.shadowOf;
16
17@SuppressWarnings({"UnusedDeclaration"})
18@Implements(AdapterView.class)
19public 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
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
50    public void setEmptyView(View emptyView) {
51        this.mEmptyView = emptyView;
52        updateEmptyStatus(adapter == null || adapter.isEmpty());
53    }
54
55    @Implementation
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
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
92    private boolean hasOnItemSelectedListener() {
93        return onItemSelectedListener != null;
94    }
95
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     */
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     */
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     */
159    public static void automaticallyUpdateRowViews(boolean shouldUpdate) {
160        automaticallyUpdateRowViews = shouldUpdate;
161    }
162
163    @Implementation
164    public int getSelectedItemPosition() {
165        return selectedPosition;
166    }
167
168    @Implementation
169    public Object getSelectedItem() {
170        int pos = getSelectedItemPosition();
171        return getItemAtPosition(pos);
172    }
173
174    @Implementation
175    public Adapter getAdapter() {
176        return adapter;
177    }
178
179    @Implementation
180    public int getCount() {
181        return itemCount;
182    }
183
184    @Implementation
185    public void setOnItemSelectedListener(AdapterView.OnItemSelectedListener listener) {
186        this.onItemSelectedListener = listener;
187    }
188
189    @Implementation
190    public final AdapterView.OnItemSelectedListener getOnItemSelectedListener() {
191        return onItemSelectedListener;
192    }
193
194    @Implementation
195    public void setOnItemClickListener(AdapterView.OnItemClickListener listener) {
196        this.onItemClickListener = listener;
197    }
198
199    @Implementation
200    public final AdapterView.OnItemClickListener getOnItemClickListener() {
201        return onItemClickListener;
202    }
203
204    @Implementation
205    public void setOnItemLongClickListener(AdapterView.OnItemLongClickListener listener) {
206        this.onItemLongClickListener = listener;
207    }
208
209    @Implementation
210    public AdapterView.OnItemLongClickListener getOnItemLongClickListener() {
211        return onItemLongClickListener;
212    }
213
214    @Implementation
215    public Object getItemAtPosition(int position) {
216        Adapter adapter = getAdapter();
217        return (adapter == null || position < 0) ? null : adapter.getItem(position);
218    }
219
220    @Implementation
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
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
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
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
259    public boolean performItemClick(int position) {
260        return realAdapterView.performItemClick(realAdapterView.getChildAt(position),
261                position, realAdapterView.getItemIdAtPosition(position));
262    }
263
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
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
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
292    public View getEmptyView() {
293        return mEmptyView;
294    }
295
296    private void update() {
297        if (!automaticallyUpdateRowViews) {
298            return;
299        }
300
301        super.removeAllViews();
302        addViews();
303    }
304
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
334        public void onChanged() {
335            invalidateAndScheduleUpdate();
336        }
337
338        @Override
339        public void onInvalidated() {
340            invalidateAndScheduleUpdate();
341        }
342    }
343}
344