1package com.xtremelabs.robolectric.shadows;
2
3import android.R;
4import android.app.AlertDialog;
5import android.content.Context;
6import android.content.DialogInterface;
7import android.view.View;
8import android.widget.Adapter;
9import android.widget.AdapterView;
10import android.widget.ArrayAdapter;
11import android.widget.Button;
12import android.widget.ListAdapter;
13import android.widget.ListView;
14import com.xtremelabs.robolectric.Robolectric;
15import com.xtremelabs.robolectric.internal.Implementation;
16import com.xtremelabs.robolectric.internal.Implements;
17import com.xtremelabs.robolectric.internal.RealObject;
18
19import java.lang.reflect.Constructor;
20
21import static com.xtremelabs.robolectric.Robolectric.getShadowApplication;
22import static com.xtremelabs.robolectric.Robolectric.shadowOf;
23
24@SuppressWarnings({"UnusedDeclaration"})
25@Implements(AlertDialog.class)
26public class ShadowAlertDialog extends ShadowDialog {
27    @RealObject
28    private AlertDialog realAlertDialog;
29
30    private CharSequence[] items;
31    private String message;
32    private DialogInterface.OnClickListener clickListener;
33    private boolean isMultiItem;
34    private boolean isSingleItem;
35    private DialogInterface.OnMultiChoiceClickListener multiChoiceClickListener;
36    private boolean[] checkedItems;
37    private int checkedItemIndex;
38    private Button positiveButton;
39    private Button negativeButton;
40    private Button neutralButton;
41    private View view;
42    private View customTitleView;
43    private ListAdapter adapter;
44    private ListView listView;
45
46    /**
47     * Non-Android accessor.
48     *
49     * @return the most recently created {@code AlertDialog}, or null if none has been created during this test run
50     */
51    public static AlertDialog getLatestAlertDialog() {
52        ShadowAlertDialog dialog = Robolectric.getShadowApplication().getLatestAlertDialog();
53        return dialog == null ? null : dialog.realAlertDialog;
54    }
55
56    @Override
57    @Implementation
58    public View findViewById(int viewId) {
59        if(view == null) {
60            return super.findViewById(viewId);
61        }
62
63        return view.findViewById(viewId);
64    }
65
66    @Implementation
67    public void setView(View view) {
68        this.view = view;
69    }
70
71    /**
72     * Resets the tracking of the most recently created {@code AlertDialog}
73     */
74    public static void reset() {
75        getShadowApplication().setLatestAlertDialog(null);
76    }
77
78    /**
79     * Simulates a click on the {@code Dialog} item indicated by {@code index}. Handles both multi- and single-choice dialogs, tracks which items are currently
80     * checked and calls listeners appropriately.
81     *
82     * @param index the index of the item to click on
83     */
84    public void clickOnItem(int index) {
85        shadowOf(realAlertDialog.getListView()).performItemClick(index);
86    }
87
88    @Implementation
89    public Button getButton(int whichButton) {
90        switch (whichButton) {
91            case AlertDialog.BUTTON_POSITIVE:
92                return positiveButton;
93            case AlertDialog.BUTTON_NEGATIVE:
94                return negativeButton;
95            case AlertDialog.BUTTON_NEUTRAL:
96                return neutralButton;
97        }
98        throw new RuntimeException("Only positive, negative, or neutral button choices are recognized");
99    }
100
101    @Implementation
102    public void setButton(int whichButton, CharSequence text, DialogInterface.OnClickListener listener) {
103        switch (whichButton) {
104            case AlertDialog.BUTTON_POSITIVE:
105                positiveButton = createButton(context, realAlertDialog, whichButton, text, listener);
106                return;
107            case AlertDialog.BUTTON_NEGATIVE:
108                negativeButton = createButton(context, realAlertDialog, whichButton, text, listener);
109                return;
110            case AlertDialog.BUTTON_NEUTRAL:
111                neutralButton = createButton(context, realAlertDialog, whichButton, text, listener);
112                return;
113        }
114        throw new RuntimeException("Only positive, negative, or neutral button choices are recognized");
115    }
116
117    private static Button createButton(final Context context, final DialogInterface dialog, final int which, CharSequence text, final DialogInterface.OnClickListener listener) {
118        if (text == null && listener == null) {
119            return null;
120        }
121        Button button = new Button(context);
122        Robolectric.shadowOf(button).setText(text); // use shadow to skip
123                                                    // i18n-strict checking
124        button.setOnClickListener(new View.OnClickListener() {
125            @Override
126            public void onClick(View v) {
127                if (listener != null) {
128                    listener.onClick(dialog, which);
129                }
130                dialog.dismiss();
131            }
132        });
133        return button;
134    }
135
136    @Implementation
137    public ListView getListView() {
138        if (listView == null) {
139            listView = new ListView(context);
140            listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
141                @Override
142                public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
143                    if (isMultiItem) {
144                        checkedItems[position] = !checkedItems[position];
145                        multiChoiceClickListener.onClick(realAlertDialog, position, checkedItems[position]);
146                    } else {
147                        if (isSingleItem) {
148                            checkedItemIndex = position;
149                        }
150                        clickListener.onClick(realAlertDialog, position);
151                    }
152                }
153            });
154        }
155        return listView;
156    }
157
158    /**
159     * Non-Android accessor.
160     *
161     * @return the items that are available to be clicked on
162     */
163    public CharSequence[] getItems() {
164        return items;
165    }
166
167    public Adapter getAdapter() {
168        return adapter;
169    }
170
171    /**
172     * Non-Android accessor.
173     *
174     * @return the message displayed in the dialog
175     */
176    public String getMessage() {
177        return message;
178    }
179
180    @Implementation
181    public void setMessage(CharSequence message) {
182        this.message = (message == null ? null : message.toString());
183    }
184
185    /**
186     * Non-Android accessor.
187     *
188     * @return an array indicating which items are and are not clicked on a multi-choice dialog
189     */
190    public boolean[] getCheckedItems() {
191        return checkedItems;
192    }
193
194    /**
195     * Non-Android accessor.
196     *
197     * @return return the index of the checked item clicked on a single-choice dialog
198     */
199    public int getCheckedItemIndex() {
200        return checkedItemIndex;
201    }
202
203    @Implementation
204    public void show() {
205        super.show();
206        if (items != null) {
207            adapter = new ArrayAdapter<CharSequence>(context, R.layout.simple_list_item_checked, R.id.text1, items);
208        }
209
210        if (adapter != null) {
211            getListView().setAdapter(adapter);
212        }
213
214
215        getShadowApplication().setLatestAlertDialog(this);
216    }
217
218    /**
219     * Non-Android accessor.
220     *
221     * @return return the view set with {@link ShadowAlertDialog.ShadowBuilder#setView(View)}
222     */
223    public View getView() {
224        return view;
225    }
226
227    /**
228     * Non-Android accessor.
229     *
230     * @return return the view set with {@link ShadowAlertDialog.ShadowBuilder#setCustomTitle(View)}
231     */
232    public View getCustomTitleView() {
233        return customTitleView;
234    }
235
236    /**
237     * Shadows the {@code android.app.AlertDialog.Builder} class.
238     */
239    @Implements(AlertDialog.Builder.class)
240    public static class ShadowBuilder {
241        @RealObject
242        private AlertDialog.Builder realBuilder;
243
244        private CharSequence[] items;
245        private ListAdapter adapter;
246        private DialogInterface.OnClickListener clickListener;
247        private DialogInterface.OnCancelListener cancelListener;
248        private String title;
249        private String message;
250        private Context context;
251        private boolean isMultiItem;
252        private DialogInterface.OnMultiChoiceClickListener multiChoiceClickListener;
253        private boolean[] checkedItems;
254        private CharSequence positiveText;
255        private DialogInterface.OnClickListener positiveListener;
256        private CharSequence negativeText;
257        private DialogInterface.OnClickListener negativeListener;
258        private CharSequence neutralText;
259        private DialogInterface.OnClickListener neutralListener;
260        private boolean isCancelable;
261        private boolean isSingleItem;
262        private int checkedItem;
263        private View view;
264        private View customTitleView;
265
266        /**
267         * just stashes the context for later use
268         *
269         * @param context the context
270         */
271        public void __constructor__(Context context) {
272            this.context = context;
273        }
274
275        /**
276         * Set a list of items to be displayed in the dialog as the content, you will be notified of the selected item via the supplied listener. This should be
277         * an array type i.e. R.array.foo
278         *
279         * @return This Builder object to allow for chaining of calls to set methods
280         */
281        @Implementation
282        public AlertDialog.Builder setItems(int itemsId, final DialogInterface.OnClickListener listener) {
283            this.isMultiItem = false;
284
285            this.items = context.getResources().getTextArray(itemsId);
286            this.clickListener = listener;
287            return realBuilder;
288        }
289
290        @Implementation(i18nSafe=false)
291        public AlertDialog.Builder setItems(CharSequence[] items, final DialogInterface.OnClickListener listener) {
292            this.isMultiItem = false;
293
294            this.items = items;
295            this.clickListener = listener;
296            return realBuilder;
297        }
298
299        @Implementation(i18nSafe=false)
300        public AlertDialog.Builder setSingleChoiceItems(CharSequence[] items, int checkedItem, final DialogInterface.OnClickListener listener) {
301            this.isSingleItem = true;
302            this.checkedItem = checkedItem;
303            this.items = items;
304            this.clickListener = listener;
305            return realBuilder;
306        }
307
308        @Implementation(i18nSafe=false)
309        public AlertDialog.Builder setSingleChoiceItems(ListAdapter adapter, int checkedItem, final DialogInterface.OnClickListener listener) {
310            this.isSingleItem = true;
311            this.checkedItem = checkedItem;
312            this.items = null;
313            this.adapter = adapter;
314            this.clickListener = listener;
315            return realBuilder;
316        }
317
318        @Implementation(i18nSafe=false)
319        public AlertDialog.Builder setMultiChoiceItems(CharSequence[] items, boolean[] checkedItems, final DialogInterface.OnMultiChoiceClickListener listener) {
320            this.isMultiItem = true;
321
322            this.items = items;
323            this.multiChoiceClickListener = listener;
324
325            if (checkedItems == null) {
326                checkedItems = new boolean[items.length];
327            } else if (checkedItems.length != items.length) {
328                throw new IllegalArgumentException("checkedItems must be the same length as items, or pass null to specify no checked items");
329            }
330            this.checkedItems = checkedItems;
331
332            return realBuilder;
333        }
334
335        @Implementation(i18nSafe=false)
336        public AlertDialog.Builder setTitle(CharSequence title) {
337            this.title = title.toString();
338            return realBuilder;
339        }
340
341
342        @Implementation
343        public AlertDialog.Builder setCustomTitle(android.view.View customTitleView) {
344            this.customTitleView = customTitleView;
345            return realBuilder;
346        }
347
348        @Implementation
349        public AlertDialog.Builder setTitle(int titleId) {
350            return setTitle(context.getResources().getString(titleId));
351        }
352
353        @Implementation(i18nSafe=false)
354        public AlertDialog.Builder setMessage(CharSequence message) {
355            this.message = message.toString();
356            return realBuilder;
357        }
358
359        @Implementation
360        public AlertDialog.Builder setMessage(int messageId) {
361            setMessage(context.getResources().getString(messageId));
362            return realBuilder;
363        }
364
365        @Implementation
366        public AlertDialog.Builder setIcon(int iconId) {
367            return realBuilder;
368        }
369
370        @Implementation
371        public AlertDialog.Builder setView(View view) {
372            this.view = view;
373            return realBuilder;
374        }
375
376        @Implementation(i18nSafe=false)
377        public AlertDialog.Builder setPositiveButton(CharSequence text, final DialogInterface.OnClickListener listener) {
378            this.positiveText = text;
379            this.positiveListener = listener;
380            return realBuilder;
381        }
382
383        @Implementation
384        public AlertDialog.Builder setPositiveButton(int positiveTextId, final DialogInterface.OnClickListener listener) {
385            return setPositiveButton(context.getResources().getText(positiveTextId), listener);
386        }
387
388        @Implementation(i18nSafe=false)
389        public AlertDialog.Builder setNegativeButton(CharSequence text, final DialogInterface.OnClickListener listener) {
390            this.negativeText = text;
391            this.negativeListener = listener;
392            return realBuilder;
393        }
394
395        @Implementation
396        public AlertDialog.Builder setNegativeButton(int negativeTextId, final DialogInterface.OnClickListener listener) {
397            return setNegativeButton(context.getResources().getString(negativeTextId), listener);
398        }
399
400        @Implementation(i18nSafe=false)
401        public AlertDialog.Builder setNeutralButton(CharSequence text, final DialogInterface.OnClickListener listener) {
402            this.neutralText = text;
403            this.neutralListener = listener;
404            return realBuilder;
405        }
406
407        @Implementation
408        public AlertDialog.Builder setNeutralButton(int neutralTextId, final DialogInterface.OnClickListener listener) {
409            return setNeutralButton(context.getResources().getText(neutralTextId), listener);
410        }
411
412
413        @Implementation
414        public AlertDialog.Builder setCancelable(boolean cancelable) {
415            this.isCancelable = cancelable;
416            return realBuilder;
417        }
418
419        @Implementation
420        public AlertDialog.Builder setOnCancelListener(DialogInterface.OnCancelListener listener) {
421            this.cancelListener = listener;
422            return realBuilder;
423        }
424
425        @Implementation
426        public AlertDialog create() {
427            AlertDialog realDialog;
428            try {
429                Constructor<AlertDialog> c = AlertDialog.class.getDeclaredConstructor(Context.class);
430                c.setAccessible(true);
431                realDialog = c.newInstance((Context) null);
432            } catch (Exception e) {
433                throw new RuntimeException(e);
434            }
435
436            ShadowAlertDialog latestAlertDialog = shadowOf(realDialog);
437            latestAlertDialog.context = context;
438            latestAlertDialog.items = items;
439            latestAlertDialog.adapter = adapter;
440            latestAlertDialog.setTitle(title);
441            latestAlertDialog.message = message;
442            latestAlertDialog.clickListener = clickListener;
443            latestAlertDialog.setOnCancelListener(cancelListener);
444            latestAlertDialog.isMultiItem = isMultiItem;
445            latestAlertDialog.isSingleItem = isSingleItem;
446            latestAlertDialog.checkedItemIndex = checkedItem;
447            latestAlertDialog.multiChoiceClickListener = multiChoiceClickListener;
448            latestAlertDialog.checkedItems = checkedItems;
449            latestAlertDialog.setView(view);
450            latestAlertDialog.positiveButton = createButton(context, realDialog, AlertDialog.BUTTON_POSITIVE, positiveText, positiveListener);
451            latestAlertDialog.negativeButton = createButton(context, realDialog, AlertDialog.BUTTON_NEGATIVE, negativeText, negativeListener);
452            latestAlertDialog.neutralButton = createButton(context, realDialog, AlertDialog.BUTTON_NEUTRAL, neutralText, neutralListener);
453            latestAlertDialog.setCancelable(isCancelable);
454            latestAlertDialog.customTitleView = customTitleView;
455            return realDialog;
456        }
457
458        @Implementation
459        public AlertDialog show() {
460            AlertDialog dialog = realBuilder.create();
461            dialog.show();
462            return dialog;
463        }
464
465        @Implementation
466        public Context getContext() {
467            return context;
468        }
469    }
470}
471