1 package com.xtremelabs.robolectric.shadows;
2 
3 import android.R;
4 import android.app.AlertDialog;
5 import android.content.Context;
6 import android.content.DialogInterface;
7 import android.view.View;
8 import android.widget.Adapter;
9 import android.widget.AdapterView;
10 import android.widget.ArrayAdapter;
11 import android.widget.Button;
12 import android.widget.ListAdapter;
13 import android.widget.ListView;
14 import com.xtremelabs.robolectric.Robolectric;
15 import com.xtremelabs.robolectric.internal.Implementation;
16 import com.xtremelabs.robolectric.internal.Implements;
17 import com.xtremelabs.robolectric.internal.RealObject;
18 
19 import java.lang.reflect.Constructor;
20 
21 import static com.xtremelabs.robolectric.Robolectric.getShadowApplication;
22 import static com.xtremelabs.robolectric.Robolectric.shadowOf;
23 
24 @SuppressWarnings({"UnusedDeclaration"})
25 @Implements(AlertDialog.class)
26 public 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      */
getLatestAlertDialog()51     public static AlertDialog getLatestAlertDialog() {
52         ShadowAlertDialog dialog = Robolectric.getShadowApplication().getLatestAlertDialog();
53         return dialog == null ? null : dialog.realAlertDialog;
54     }
55 
56     @Override
57     @Implementation
findViewById(int viewId)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
setView(View view)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      */
reset()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      */
clickOnItem(int index)84     public void clickOnItem(int index) {
85         shadowOf(realAlertDialog.getListView()).performItemClick(index);
86     }
87 
88     @Implementation
getButton(int whichButton)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
setButton(int whichButton, CharSequence text, DialogInterface.OnClickListener listener)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 
createButton(final Context context, final DialogInterface dialog, final int which, CharSequence text, final DialogInterface.OnClickListener listener)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
getListView()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      */
getItems()163     public CharSequence[] getItems() {
164         return items;
165     }
166 
getAdapter()167     public Adapter getAdapter() {
168         return adapter;
169     }
170 
171     /**
172      * Non-Android accessor.
173      *
174      * @return the message displayed in the dialog
175      */
getMessage()176     public String getMessage() {
177         return message;
178     }
179 
180     @Implementation
setMessage(CharSequence message)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      */
getCheckedItems()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      */
getCheckedItemIndex()199     public int getCheckedItemIndex() {
200         return checkedItemIndex;
201     }
202 
203     @Implementation
show()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      */
getView()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      */
getCustomTitleView()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          */
__constructor__(Context context)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
setItems(int itemsId, final DialogInterface.OnClickListener listener)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)
setItems(CharSequence[] items, final DialogInterface.OnClickListener listener)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)
setSingleChoiceItems(CharSequence[] items, int checkedItem, final DialogInterface.OnClickListener listener)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)
setSingleChoiceItems(ListAdapter adapter, int checkedItem, final DialogInterface.OnClickListener listener)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)
setMultiChoiceItems(CharSequence[] items, boolean[] checkedItems, final DialogInterface.OnMultiChoiceClickListener listener)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)
setTitle(CharSequence title)336         public AlertDialog.Builder setTitle(CharSequence title) {
337             this.title = title.toString();
338             return realBuilder;
339         }
340 
341 
342         @Implementation
setCustomTitle(android.view.View customTitleView)343         public AlertDialog.Builder setCustomTitle(android.view.View customTitleView) {
344             this.customTitleView = customTitleView;
345             return realBuilder;
346         }
347 
348         @Implementation
setTitle(int titleId)349         public AlertDialog.Builder setTitle(int titleId) {
350             return setTitle(context.getResources().getString(titleId));
351         }
352 
353         @Implementation(i18nSafe=false)
setMessage(CharSequence message)354         public AlertDialog.Builder setMessage(CharSequence message) {
355             this.message = message.toString();
356             return realBuilder;
357         }
358 
359         @Implementation
setMessage(int messageId)360         public AlertDialog.Builder setMessage(int messageId) {
361             setMessage(context.getResources().getString(messageId));
362             return realBuilder;
363         }
364 
365         @Implementation
setIcon(int iconId)366         public AlertDialog.Builder setIcon(int iconId) {
367             return realBuilder;
368         }
369 
370         @Implementation
setView(View view)371         public AlertDialog.Builder setView(View view) {
372             this.view = view;
373             return realBuilder;
374         }
375 
376         @Implementation(i18nSafe=false)
setPositiveButton(CharSequence text, final DialogInterface.OnClickListener listener)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
setPositiveButton(int positiveTextId, final DialogInterface.OnClickListener listener)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)
setNegativeButton(CharSequence text, final DialogInterface.OnClickListener listener)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
setNegativeButton(int negativeTextId, final DialogInterface.OnClickListener listener)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)
setNeutralButton(CharSequence text, final DialogInterface.OnClickListener listener)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
setNeutralButton(int neutralTextId, final DialogInterface.OnClickListener listener)408         public AlertDialog.Builder setNeutralButton(int neutralTextId, final DialogInterface.OnClickListener listener) {
409             return setNeutralButton(context.getResources().getText(neutralTextId), listener);
410         }
411 
412 
413         @Implementation
setCancelable(boolean cancelable)414         public AlertDialog.Builder setCancelable(boolean cancelable) {
415             this.isCancelable = cancelable;
416             return realBuilder;
417         }
418 
419         @Implementation
setOnCancelListener(DialogInterface.OnCancelListener listener)420         public AlertDialog.Builder setOnCancelListener(DialogInterface.OnCancelListener listener) {
421             this.cancelListener = listener;
422             return realBuilder;
423         }
424 
425         @Implementation
create()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
show()459         public AlertDialog show() {
460             AlertDialog dialog = realBuilder.create();
461             dialog.show();
462             return dialog;
463         }
464 
465         @Implementation
getContext()466         public Context getContext() {
467             return context;
468         }
469     }
470 }
471