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