1 package org.robolectric.shadows;
2 
3 import static android.os.Build.VERSION_CODES.JELLY_BEAN;
4 import static android.os.Build.VERSION_CODES.LOLLIPOP;
5 import static android.os.Build.VERSION_CODES.M;
6 import static org.robolectric.shadow.api.Shadow.directlyOn;
7 
8 import android.R;
9 import android.app.Activity;
10 import android.app.ActivityManager;
11 import android.app.ActivityThread;
12 import android.app.Application;
13 import android.app.Dialog;
14 import android.app.Instrumentation;
15 import android.content.ComponentName;
16 import android.content.Context;
17 import android.content.Intent;
18 import android.content.pm.ActivityInfo;
19 import android.content.pm.PackageManager;
20 import android.content.pm.PackageManager.NameNotFoundException;
21 import android.content.res.Configuration;
22 import android.database.Cursor;
23 import android.os.Build;
24 import android.os.Bundle;
25 import android.os.IBinder;
26 import android.text.Selection;
27 import android.text.SpannableStringBuilder;
28 import android.view.LayoutInflater;
29 import android.view.Menu;
30 import android.view.MenuInflater;
31 import android.view.View;
32 import android.view.ViewGroup;
33 import android.view.ViewRootImpl;
34 import android.view.Window;
35 import com.android.internal.app.IVoiceInteractor;
36 import java.lang.reflect.InvocationTargetException;
37 import java.lang.reflect.Method;
38 import java.util.ArrayList;
39 import java.util.HashMap;
40 import java.util.List;
41 import java.util.Map;
42 import org.robolectric.RuntimeEnvironment;
43 import org.robolectric.annotation.HiddenApi;
44 import org.robolectric.annotation.Implementation;
45 import org.robolectric.annotation.Implements;
46 import org.robolectric.annotation.RealObject;
47 import org.robolectric.fakes.RoboMenuItem;
48 import org.robolectric.shadow.api.Shadow;
49 import org.robolectric.util.ReflectionHelpers;
50 
51 @Implements(Activity.class)
52 public class ShadowActivity extends ShadowContextThemeWrapper {
53 
54   @RealObject
55   protected Activity realActivity;
56 
57   private int resultCode;
58   private Intent resultIntent;
59   private Activity parent;
60   private int requestedOrientation = -1;
61   private View currentFocus;
62   private Integer lastShownDialogId = null;
63   private int pendingTransitionEnterAnimResId = -1;
64   private int pendingTransitionExitAnimResId = -1;
65   private Object lastNonConfigurationInstance;
66   private Map<Integer, Dialog> dialogForId = new HashMap<>();
67   private ArrayList<Cursor> managedCursors = new ArrayList<>();
68   private int mDefaultKeyMode = Activity.DEFAULT_KEYS_DISABLE;
69   private SpannableStringBuilder mDefaultKeySsb = null;
70   private int streamType = -1;
71   private boolean mIsTaskRoot = true;
72   private Menu optionsMenu;
73   private ComponentName callingActivity;
74   private String callingPackage;
75   private boolean isLockTask;
76   private PermissionsRequest lastRequestedPermission;
77 
setApplication(Application application)78   public void setApplication(Application application) {
79     ReflectionHelpers.setField(realActivity, "mApplication", application);
80   }
81 
callAttach(Intent intent)82   public void callAttach(Intent intent) {
83     int apiLevel = RuntimeEnvironment.getApiLevel();
84     Application application = RuntimeEnvironment.application;
85     Context baseContext = RuntimeEnvironment.application.getBaseContext();
86     Class<?> nonConfigurationInstancesClass = getNonConfigurationClass();
87 
88     ActivityInfo activityInfo;
89     try {
90       activityInfo = application.getPackageManager().getActivityInfo(new ComponentName(application.getPackageName(), realActivity.getClass().getName()), PackageManager.GET_ACTIVITIES | PackageManager.GET_META_DATA);
91     } catch (NameNotFoundException e) {
92       throw new RuntimeException();
93     }
94 
95     CharSequence activityTitle = activityInfo.loadLabel(baseContext.getPackageManager());
96 
97     Instrumentation instrumentation =
98         ((ActivityThread) RuntimeEnvironment.getActivityThread()).getInstrumentation();
99     if (apiLevel <= Build.VERSION_CODES.KITKAT) {
100       ReflectionHelpers.callInstanceMethod(
101           Activity.class,
102           realActivity,
103           "attach",
104           ReflectionHelpers.ClassParameter.from(Context.class, baseContext),
105           ReflectionHelpers.ClassParameter.from(
106               ActivityThread.class, RuntimeEnvironment.getActivityThread()),
107           ReflectionHelpers.ClassParameter.from(Instrumentation.class, instrumentation),
108           ReflectionHelpers.ClassParameter.from(IBinder.class, null),
109           ReflectionHelpers.ClassParameter.from(int.class, 0),
110           ReflectionHelpers.ClassParameter.from(Application.class, application),
111           ReflectionHelpers.ClassParameter.from(Intent.class, intent),
112           ReflectionHelpers.ClassParameter.from(ActivityInfo.class, activityInfo),
113           ReflectionHelpers.ClassParameter.from(CharSequence.class, activityTitle),
114           ReflectionHelpers.ClassParameter.from(Activity.class, null),
115           ReflectionHelpers.ClassParameter.from(String.class, "id"),
116           ReflectionHelpers.ClassParameter.from(nonConfigurationInstancesClass, null),
117           ReflectionHelpers.ClassParameter.from(
118               Configuration.class, application.getResources().getConfiguration()));
119     } else if (apiLevel <= Build.VERSION_CODES.LOLLIPOP) {
120       ReflectionHelpers.callInstanceMethod(
121           Activity.class,
122           realActivity,
123           "attach",
124           ReflectionHelpers.ClassParameter.from(Context.class, baseContext),
125           ReflectionHelpers.ClassParameter.from(
126               ActivityThread.class, RuntimeEnvironment.getActivityThread()),
127           ReflectionHelpers.ClassParameter.from(Instrumentation.class, instrumentation),
128           ReflectionHelpers.ClassParameter.from(IBinder.class, null),
129           ReflectionHelpers.ClassParameter.from(int.class, 0),
130           ReflectionHelpers.ClassParameter.from(Application.class, application),
131           ReflectionHelpers.ClassParameter.from(Intent.class, intent),
132           ReflectionHelpers.ClassParameter.from(ActivityInfo.class, activityInfo),
133           ReflectionHelpers.ClassParameter.from(CharSequence.class, activityTitle),
134           ReflectionHelpers.ClassParameter.from(Activity.class, null),
135           ReflectionHelpers.ClassParameter.from(String.class, "id"),
136           ReflectionHelpers.ClassParameter.from(nonConfigurationInstancesClass, null),
137           ReflectionHelpers.ClassParameter.from(
138               Configuration.class, application.getResources().getConfiguration()),
139           ReflectionHelpers.ClassParameter.from(IVoiceInteractor.class, null)); // ADDED
140     } else if (apiLevel <= Build.VERSION_CODES.M) {
141       ReflectionHelpers.callInstanceMethod(
142           Activity.class,
143           realActivity,
144           "attach",
145           ReflectionHelpers.ClassParameter.from(Context.class, baseContext),
146           ReflectionHelpers.ClassParameter.from(
147               ActivityThread.class, RuntimeEnvironment.getActivityThread()),
148           ReflectionHelpers.ClassParameter.from(Instrumentation.class, instrumentation),
149           ReflectionHelpers.ClassParameter.from(IBinder.class, null),
150           ReflectionHelpers.ClassParameter.from(int.class, 0),
151           ReflectionHelpers.ClassParameter.from(Application.class, application),
152           ReflectionHelpers.ClassParameter.from(Intent.class, intent),
153           ReflectionHelpers.ClassParameter.from(ActivityInfo.class, activityInfo),
154           ReflectionHelpers.ClassParameter.from(CharSequence.class, activityTitle),
155           ReflectionHelpers.ClassParameter.from(Activity.class, null),
156           ReflectionHelpers.ClassParameter.from(String.class, "id"),
157           ReflectionHelpers.ClassParameter.from(nonConfigurationInstancesClass, null),
158           ReflectionHelpers.ClassParameter.from(
159               Configuration.class, application.getResources().getConfiguration()),
160           ReflectionHelpers.ClassParameter.from(String.class, "referrer"),
161           ReflectionHelpers.ClassParameter.from(
162               IVoiceInteractor.class, null)); // SAME AS LOLLIPOP ---------------------------
163     } else if (apiLevel <= Build.VERSION_CODES.N_MR1) {
164       ReflectionHelpers.callInstanceMethod(
165           Activity.class,
166           realActivity,
167           "attach",
168           ReflectionHelpers.ClassParameter.from(Context.class, baseContext),
169           ReflectionHelpers.ClassParameter.from(
170               ActivityThread.class, RuntimeEnvironment.getActivityThread()),
171           ReflectionHelpers.ClassParameter.from(Instrumentation.class, instrumentation),
172           ReflectionHelpers.ClassParameter.from(IBinder.class, null),
173           ReflectionHelpers.ClassParameter.from(int.class, 0),
174           ReflectionHelpers.ClassParameter.from(Application.class, application),
175           ReflectionHelpers.ClassParameter.from(Intent.class, intent),
176           ReflectionHelpers.ClassParameter.from(ActivityInfo.class, activityInfo),
177           ReflectionHelpers.ClassParameter.from(CharSequence.class, activityTitle),
178           ReflectionHelpers.ClassParameter.from(Activity.class, null),
179           ReflectionHelpers.ClassParameter.from(String.class, "id"),
180           ReflectionHelpers.ClassParameter.from(nonConfigurationInstancesClass, null),
181           ReflectionHelpers.ClassParameter.from(
182               Configuration.class, application.getResources().getConfiguration()),
183           ReflectionHelpers.ClassParameter.from(String.class, "referrer"),
184           ReflectionHelpers.ClassParameter.from(IVoiceInteractor.class, null),
185           ReflectionHelpers.ClassParameter.from(Window.class, null) // ADDED
186           );
187     } else if (apiLevel <= Build.VERSION_CODES.P) {
188       ReflectionHelpers.callInstanceMethod(
189           Activity.class,
190           realActivity,
191           "attach",
192           ReflectionHelpers.ClassParameter.from(Context.class, baseContext),
193           ReflectionHelpers.ClassParameter.from(
194               ActivityThread.class, RuntimeEnvironment.getActivityThread()),
195           ReflectionHelpers.ClassParameter.from(Instrumentation.class, instrumentation),
196           ReflectionHelpers.ClassParameter.from(IBinder.class, null),
197           ReflectionHelpers.ClassParameter.from(int.class, 0),
198           ReflectionHelpers.ClassParameter.from(Application.class, application),
199           ReflectionHelpers.ClassParameter.from(Intent.class, intent),
200           ReflectionHelpers.ClassParameter.from(ActivityInfo.class, activityInfo),
201           ReflectionHelpers.ClassParameter.from(CharSequence.class, activityTitle),
202           ReflectionHelpers.ClassParameter.from(Activity.class, null),
203           ReflectionHelpers.ClassParameter.from(String.class, "id"),
204           ReflectionHelpers.ClassParameter.from(nonConfigurationInstancesClass, null),
205           ReflectionHelpers.ClassParameter.from(
206               Configuration.class, application.getResources().getConfiguration()),
207           ReflectionHelpers.ClassParameter.from(String.class, "referrer"),
208           ReflectionHelpers.ClassParameter.from(IVoiceInteractor.class, null),
209           ReflectionHelpers.ClassParameter.from(Window.class, null),
210           ReflectionHelpers.ClassParameter.from(
211               ViewRootImpl.ActivityConfigCallback.class, null) // ADDED
212           );
213     } else if (apiLevel <= Build.VERSION_CODES.R) {
214       ReflectionHelpers.callInstanceMethod(
215           Activity.class,
216           realActivity,
217           "attach",
218           ReflectionHelpers.ClassParameter.from(Context.class, baseContext),
219           ReflectionHelpers.ClassParameter.from(
220                   ActivityThread.class, RuntimeEnvironment.getActivityThread()),
221           ReflectionHelpers.ClassParameter.from(Instrumentation.class, instrumentation),
222           ReflectionHelpers.ClassParameter.from(IBinder.class, null),
223           ReflectionHelpers.ClassParameter.from(int.class, 0),
224           ReflectionHelpers.ClassParameter.from(Application.class, application),
225           ReflectionHelpers.ClassParameter.from(Intent.class, intent),
226           ReflectionHelpers.ClassParameter.from(ActivityInfo.class, activityInfo),
227           ReflectionHelpers.ClassParameter.from(CharSequence.class, activityTitle),
228           ReflectionHelpers.ClassParameter.from(Activity.class, null),
229           ReflectionHelpers.ClassParameter.from(String.class, "id"),
230           ReflectionHelpers.ClassParameter.from(nonConfigurationInstancesClass, null),
231           ReflectionHelpers.ClassParameter.from(
232                   Configuration.class, application.getResources().getConfiguration()),
233           ReflectionHelpers.ClassParameter.from(String.class, "referrer"),
234           ReflectionHelpers.ClassParameter.from(IVoiceInteractor.class, null),
235           ReflectionHelpers.ClassParameter.from(Window.class, null),
236           ReflectionHelpers.ClassParameter.from(
237                   ViewRootImpl.ActivityConfigCallback.class, null),
238           ReflectionHelpers.ClassParameter.from(IBinder.class, null) // ADDED
239       );
240     } else if (apiLevel >= Build.VERSION_CODES.S) {
241       ReflectionHelpers.callInstanceMethod(
242               Activity.class,
243               realActivity,
244               "attach",
245               ReflectionHelpers.ClassParameter.from(Context.class, baseContext),
246               ReflectionHelpers.ClassParameter.from(
247                       ActivityThread.class, RuntimeEnvironment.getActivityThread()),
248               ReflectionHelpers.ClassParameter.from(Instrumentation.class, instrumentation),
249               ReflectionHelpers.ClassParameter.from(IBinder.class, null),
250               ReflectionHelpers.ClassParameter.from(int.class, 0),
251               ReflectionHelpers.ClassParameter.from(Application.class, application),
252               ReflectionHelpers.ClassParameter.from(Intent.class, intent),
253               ReflectionHelpers.ClassParameter.from(ActivityInfo.class, activityInfo),
254               ReflectionHelpers.ClassParameter.from(CharSequence.class, activityTitle),
255               ReflectionHelpers.ClassParameter.from(Activity.class, null),
256               ReflectionHelpers.ClassParameter.from(String.class, "id"),
257               ReflectionHelpers.ClassParameter.from(nonConfigurationInstancesClass, null),
258               ReflectionHelpers.ClassParameter.from(
259                       Configuration.class, application.getResources().getConfiguration()),
260               ReflectionHelpers.ClassParameter.from(String.class, "referrer"),
261               ReflectionHelpers.ClassParameter.from(IVoiceInteractor.class, null),
262               ReflectionHelpers.ClassParameter.from(Window.class, null),
263               ReflectionHelpers.ClassParameter.from(
264                       ViewRootImpl.ActivityConfigCallback.class, null),
265               ReflectionHelpers.ClassParameter.from(IBinder.class, null), // ADDED
266               ReflectionHelpers.ClassParameter.from(IBinder.class, null) // ADDED
267       );
268     } else {
269       throw new RuntimeException("Could not find AndroidRuntimeAdapter for API level: " + apiLevel);
270     }
271 
272     int theme = activityInfo.getThemeResource();
273     if (theme != 0) {
274       realActivity.setTheme(theme);
275     }
276   }
277 
getNonConfigurationClass()278   private Class<?> getNonConfigurationClass() {
279     try {
280       return getClass().getClassLoader().loadClass("android.app.Activity$NonConfigurationInstances");
281     } catch (ClassNotFoundException e) {
282       throw new RuntimeException(e);
283     }
284   }
285 
setCallingActivity(ComponentName activityName)286   public void setCallingActivity(ComponentName activityName) {
287     callingActivity = activityName;
288   }
289 
290   @Implementation
getCallingActivity()291   protected ComponentName getCallingActivity() {
292     return callingActivity;
293   }
294 
setCallingPackage(String packageName)295   public void setCallingPackage(String packageName) {
296     callingPackage = packageName;
297   }
298 
299   @Implementation
getCallingPackage()300   protected String getCallingPackage() {
301     return callingPackage;
302   }
303 
304   @Implementation
setDefaultKeyMode(int keyMode)305   protected void setDefaultKeyMode(int keyMode) {
306     mDefaultKeyMode = keyMode;
307 
308     // Some modes use a SpannableStringBuilder to track & dispatch input events
309     // This list must remain in sync with the switch in onKeyDown()
310     switch (mDefaultKeyMode) {
311       case Activity.DEFAULT_KEYS_DISABLE:
312       case Activity.DEFAULT_KEYS_SHORTCUT:
313         mDefaultKeySsb = null;      // not used in these modes
314         break;
315       case Activity.DEFAULT_KEYS_DIALER:
316       case Activity.DEFAULT_KEYS_SEARCH_LOCAL:
317       case Activity.DEFAULT_KEYS_SEARCH_GLOBAL:
318         mDefaultKeySsb = new SpannableStringBuilder();
319         Selection.setSelection(mDefaultKeySsb, 0);
320         break;
321       default:
322         throw new IllegalArgumentException();
323     }
324   }
325 
getDefaultKeymode()326   public int getDefaultKeymode() {
327     return mDefaultKeyMode;
328   }
329 
330   @Implementation
setResult(int resultCode)331   protected final void setResult(int resultCode) {
332     this.resultCode = resultCode;
333   }
334 
335   @Implementation
setResult(int resultCode, Intent data)336   protected final void setResult(int resultCode, Intent data) {
337     this.resultCode = resultCode;
338     this.resultIntent = data;
339   }
340 
341   @Implementation
getLayoutInflater()342   protected LayoutInflater getLayoutInflater() {
343     return LayoutInflater.from(realActivity);
344   }
345 
346   @Implementation
getMenuInflater()347   protected MenuInflater getMenuInflater() {
348     return new MenuInflater(realActivity);
349   }
350 
351   /**
352    * Checks to ensure that the{@code contentView} has been set
353    *
354    * @param id ID of the view to find
355    * @return the view
356    * @throws RuntimeException if the {@code contentView} has not been called first
357    */
358   @Implementation
findViewById(int id)359   protected View findViewById(int id) {
360     return getWindow().findViewById(id);
361   }
362 
363   @Implementation
getParent()364   protected final Activity getParent() {
365     return parent;
366   }
367 
368   /**
369    * Allow setting of Parent fragmentActivity (for unit testing purposes only)
370    *
371    * @param parent Parent fragmentActivity to set on this fragmentActivity
372    */
373   @HiddenApi @Implementation
setParent(Activity parent)374   public void setParent(Activity parent) {
375     this.parent = parent;
376   }
377 
378   @Implementation
onBackPressed()379   protected void onBackPressed() {
380     finish();
381   }
382 
383   @Implementation
finish()384   protected void finish() {
385     // Sets the mFinished field in the real activity so NoDisplay activities can be tested.
386     ReflectionHelpers.setField(Activity.class, realActivity, "mFinished", true);
387   }
388 
389   @Implementation(minSdk = LOLLIPOP)
finishAndRemoveTask()390   protected void finishAndRemoveTask() {
391     // Sets the mFinished field in the real activity so NoDisplay activities can be tested.
392     ReflectionHelpers.setField(Activity.class, realActivity, "mFinished", true);
393   }
394 
395   @Implementation(minSdk = JELLY_BEAN)
finishAffinity()396   protected void finishAffinity() {
397     // Sets the mFinished field in the real activity so NoDisplay activities can be tested.
398     ReflectionHelpers.setField(Activity.class, realActivity, "mFinished", true);
399   }
400 
resetIsFinishing()401   public void resetIsFinishing() {
402     ReflectionHelpers.setField(Activity.class, realActivity, "mFinished", false);
403   }
404 
405   /**
406    * Returns whether {@link #finish()} was called.
407    *
408    * @deprecated Use {@link Activity#isFinishing()} instead.
409    */
410   @Deprecated
isFinishing()411   public boolean isFinishing() {
412     return directlyOn(realActivity, Activity.class).isFinishing();
413   }
414 
415   /**
416    * Constructs a new Window (a {@link com.android.internal.policy.impl.PhoneWindow}) if no window
417    * has previously been set.
418    *
419    * @return the window associated with this Activity
420    */
421   @Implementation
getWindow()422   protected Window getWindow() {
423     Window window = directlyOn(realActivity, Activity.class).getWindow();
424 
425     if (window == null) {
426       try {
427         window = ShadowWindow.create(realActivity);
428         setWindow(window);
429       } catch (Exception e) {
430         throw new RuntimeException("Window creation failed!", e);
431       }
432     }
433 
434     return window;
435   }
436 
setWindow(Window window)437   public void setWindow(Window window) {
438     ReflectionHelpers.setField(realActivity, "mWindow", window);
439   }
440 
441   @Implementation
runOnUiThread(Runnable action)442   protected void runOnUiThread(Runnable action) {
443     ShadowApplication.getInstance().getForegroundThreadScheduler().post(action);
444   }
445 
446   @Implementation
setRequestedOrientation(int requestedOrientation)447   protected void setRequestedOrientation(int requestedOrientation) {
448     if (getParent() != null) {
449       getParent().setRequestedOrientation(requestedOrientation);
450     } else {
451       this.requestedOrientation = requestedOrientation;
452     }
453   }
454 
455   @Implementation
getRequestedOrientation()456   protected int getRequestedOrientation() {
457     if (getParent() != null) {
458       return getParent().getRequestedOrientation();
459     } else {
460       return this.requestedOrientation;
461     }
462   }
463 
464   @Implementation
getTaskId()465   protected int getTaskId() {
466     return 0;
467   }
468 
469   /**
470    * @return the {@code contentView} set by one of the {@code setContentView()} methods
471    */
getContentView()472   public View getContentView() {
473     return ((ViewGroup) getWindow().findViewById(R.id.content)).getChildAt(0);
474   }
475 
476   /**
477    * @return the {@code resultCode} set by one of the {@code setResult()} methods
478    */
getResultCode()479   public int getResultCode() {
480     return resultCode;
481   }
482 
483   /**
484    * @return the {@code Intent} set by {@link #setResult(int, android.content.Intent)}
485    */
getResultIntent()486   public Intent getResultIntent() {
487     return resultIntent;
488   }
489 
490   /**
491    * Consumes and returns the next {@code Intent} on the
492    * started activities for results stack.
493    *
494    * @return the next started {@code Intent} for an activity, wrapped in
495    *         an {@link ShadowActivity.IntentForResult} object
496    */
getNextStartedActivityForResult()497   public IntentForResult getNextStartedActivityForResult() {
498     ActivityThread activityThread = (ActivityThread) RuntimeEnvironment.getActivityThread();
499     ShadowInstrumentation shadowInstrumentation = Shadow.extract(activityThread.getInstrumentation());
500     return shadowInstrumentation.getNextStartedActivityForResult();
501   }
502 
503   /**
504    * Returns the most recent {@code Intent} started by
505    * {@link Activity#startActivityForResult(Intent, int)} without consuming it.
506    *
507    * @return the most recently started {@code Intent}, wrapped in
508    *         an {@link ShadowActivity.IntentForResult} object
509    */
peekNextStartedActivityForResult()510   public IntentForResult peekNextStartedActivityForResult() {
511     ActivityThread activityThread = (ActivityThread) RuntimeEnvironment.getActivityThread();
512     ShadowInstrumentation shadowInstrumentation = Shadow.extract(activityThread.getInstrumentation());
513     return shadowInstrumentation.peekNextStartedActivityForResult();
514   }
515 
516   @Implementation
getLastNonConfigurationInstance()517   protected Object getLastNonConfigurationInstance() {
518     return lastNonConfigurationInstance;
519   }
520 
setLastNonConfigurationInstance(Object lastNonConfigurationInstance)521   public void setLastNonConfigurationInstance(Object lastNonConfigurationInstance) {
522     this.lastNonConfigurationInstance = lastNonConfigurationInstance;
523   }
524 
525   /**
526    * @param view View to focus.
527    */
setCurrentFocus(View view)528   public void setCurrentFocus(View view) {
529     currentFocus = view;
530   }
531 
532   @Implementation
getCurrentFocus()533   protected View getCurrentFocus() {
534     return currentFocus;
535   }
536 
getPendingTransitionEnterAnimationResourceId()537   public int getPendingTransitionEnterAnimationResourceId() {
538     return pendingTransitionEnterAnimResId;
539   }
540 
getPendingTransitionExitAnimationResourceId()541   public int getPendingTransitionExitAnimationResourceId() {
542     return pendingTransitionExitAnimResId;
543   }
544 
545   @Implementation
onCreateOptionsMenu(Menu menu)546   protected boolean onCreateOptionsMenu(Menu menu) {
547     optionsMenu = menu;
548     return directlyOn(realActivity, Activity.class).onCreateOptionsMenu(menu);
549   }
550 
551   /**
552    * Return the options menu.
553    *
554    * @return  Options menu.
555    */
getOptionsMenu()556   public Menu getOptionsMenu() {
557     return optionsMenu;
558   }
559 
560   /**
561    * Perform a click on a menu item.
562    *
563    * @param menuItemResId Menu item resource ID.
564    * @return True if the click was handled, false otherwise.
565    */
clickMenuItem(int menuItemResId)566   public boolean clickMenuItem(int menuItemResId) {
567     final RoboMenuItem item = new RoboMenuItem(menuItemResId);
568     return realActivity.onMenuItemSelected(Window.FEATURE_OPTIONS_PANEL, item);
569   }
570 
571   /** For internal use only. Not for public use. */
callOnActivityResult(int requestCode, int resultCode, Intent resultData)572   public void callOnActivityResult(int requestCode, int resultCode, Intent resultData) {
573     final ActivityInvoker invoker = new ActivityInvoker();
574     invoker
575         .call("onActivityResult", Integer.TYPE, Integer.TYPE, Intent.class)
576         .with(requestCode, resultCode, resultData);
577   }
578 
579   /**
580    * Container object to hold an Intent, together with the requestCode used
581    * in a call to {@code Activity.startActivityForResult(Intent, int)}
582    */
583   public static class IntentForResult {
584     public Intent intent;
585     public int requestCode;
586     public Bundle options;
587 
IntentForResult(Intent intent, int requestCode)588     public IntentForResult(Intent intent, int requestCode) {
589       this.intent = intent;
590       this.requestCode = requestCode;
591       this.options = null;
592     }
593 
IntentForResult(Intent intent, int requestCode, Bundle options)594     public IntentForResult(Intent intent, int requestCode, Bundle options) {
595       this.intent = intent;
596       this.requestCode = requestCode;
597       this.options = options;
598     }
599   }
600 
receiveResult(Intent requestIntent, int resultCode, Intent resultIntent)601   public void receiveResult(Intent requestIntent, int resultCode, Intent resultIntent) {
602     ActivityThread activityThread = (ActivityThread) RuntimeEnvironment.getActivityThread();
603     ShadowInstrumentation shadowInstrumentation = Shadow.extract(activityThread.getInstrumentation());
604     int requestCode = shadowInstrumentation.getRequestCodeForIntent(requestIntent);
605 
606     callOnActivityResult(requestCode, resultCode, resultIntent);
607   }
608 
609   @Implementation
showDialog(int id)610   protected final void showDialog(int id) {
611     showDialog(id, null);
612   }
613 
614   @Implementation
dismissDialog(int id)615   protected final void dismissDialog(int id) {
616     final Dialog dialog = dialogForId.get(id);
617     if (dialog == null) {
618       throw new IllegalArgumentException();
619     }
620 
621     dialog.dismiss();
622   }
623 
624   @Implementation
removeDialog(int id)625   protected final void removeDialog(int id) {
626     dialogForId.remove(id);
627   }
628 
629   @Implementation
showDialog(int id, Bundle bundle)630   protected final boolean showDialog(int id, Bundle bundle) {
631     this.lastShownDialogId = id;
632     Dialog dialog = dialogForId.get(id);
633 
634     if (dialog == null) {
635       final ActivityInvoker invoker = new ActivityInvoker();
636       dialog = (Dialog) invoker.call("onCreateDialog", Integer.TYPE).with(id);
637       if (dialog == null) {
638         return false;
639       }
640       if (bundle == null) {
641         invoker.call("onPrepareDialog", Integer.TYPE, Dialog.class).with(id, dialog);
642       } else {
643         invoker.call("onPrepareDialog", Integer.TYPE, Dialog.class, Bundle.class).with(id, dialog, bundle);
644       }
645 
646       dialogForId.put(id, dialog);
647     }
648 
649     dialog.show();
650     return true;
651   }
652 
setIsTaskRoot(boolean isRoot)653   public void setIsTaskRoot(boolean isRoot) {
654     mIsTaskRoot = isRoot;
655   }
656 
657   @Implementation
isTaskRoot()658   protected final boolean isTaskRoot() {
659     return mIsTaskRoot;
660   }
661 
662   /**
663    * @return the dialog resource id passed into
664    *         {@code Activity.showDialog(int, Bundle)} or {@code Activity.showDialog(int)}
665    */
getLastShownDialogId()666   public Integer getLastShownDialogId() {
667     return lastShownDialogId;
668   }
669 
hasCancelledPendingTransitions()670   public boolean hasCancelledPendingTransitions() {
671     return pendingTransitionEnterAnimResId == 0 && pendingTransitionExitAnimResId == 0;
672   }
673 
674   @Implementation
overridePendingTransition(int enterAnim, int exitAnim)675   protected void overridePendingTransition(int enterAnim, int exitAnim) {
676     pendingTransitionEnterAnimResId = enterAnim;
677     pendingTransitionExitAnimResId = exitAnim;
678   }
679 
getDialogById(int dialogId)680   public Dialog getDialogById(int dialogId) {
681     return dialogForId.get(dialogId);
682   }
683 
684   @Implementation
recreate()685   protected void recreate() {
686     Bundle outState = new Bundle();
687     final ActivityInvoker invoker = new ActivityInvoker();
688 
689     invoker.call("onSaveInstanceState", Bundle.class).with(outState);
690     invoker.call("onPause").withNothing();
691     invoker.call("onStop").withNothing();
692 
693     Object nonConfigInstance = invoker.call("onRetainNonConfigurationInstance").withNothing();
694     setLastNonConfigurationInstance(nonConfigInstance);
695 
696     invoker.call("onDestroy").withNothing();
697     invoker.call("onCreate", Bundle.class).with(outState);
698     invoker.call("onStart").withNothing();
699     invoker.call("onRestoreInstanceState", Bundle.class).with(outState);
700     invoker.call("onResume").withNothing();
701   }
702 
703   @Implementation
startManagingCursor(Cursor c)704   protected void startManagingCursor(Cursor c) {
705     managedCursors.add(c);
706   }
707 
708   @Implementation
stopManagingCursor(Cursor c)709   protected void stopManagingCursor(Cursor c) {
710     managedCursors.remove(c);
711   }
712 
getManagedCursors()713   public List<Cursor> getManagedCursors() {
714     return managedCursors;
715   }
716 
717   @Implementation
setVolumeControlStream(int streamType)718   protected final void setVolumeControlStream(int streamType) {
719     this.streamType = streamType;
720   }
721 
722   @Implementation
getVolumeControlStream()723   protected final int getVolumeControlStream() {
724     return streamType;
725   }
726 
727   @Implementation(minSdk = M)
requestPermissions(String[] permissions, int requestCode)728   protected final void requestPermissions(String[] permissions, int requestCode) {
729     lastRequestedPermission = new PermissionsRequest(permissions, requestCode);
730   }
731 
732   /**
733    * Starts a lock task.
734    *
735    * <p>The status of the lock task can be verified using {@link #isLockTask} method. Otherwise this
736    * implementation has no effect.
737    */
738   @Implementation(minSdk = LOLLIPOP)
startLockTask()739   protected void startLockTask() {
740     Shadow.<ShadowActivityManager>extract(getActivityManager())
741         .setLockTaskModeState(ActivityManager.LOCK_TASK_MODE_LOCKED);
742   }
743 
744   /**
745    * Stops a lock task.
746    *
747    * <p>The status of the lock task can be verified using {@link #isLockTask} method. Otherwise this
748    * implementation has no effect.
749    */
750   @Implementation(minSdk = LOLLIPOP)
stopLockTask()751   protected void stopLockTask() {
752     Shadow.<ShadowActivityManager>extract(getActivityManager())
753         .setLockTaskModeState(ActivityManager.LOCK_TASK_MODE_NONE);
754   }
755 
756   /**
757    * Returns if the activity is in the lock task mode.
758    *
759    * @deprecated Use {@link ActivityManager#getLockTaskModeState} instead.
760    */
761   @Deprecated
isLockTask()762   public boolean isLockTask() {
763     return getActivityManager().isInLockTaskMode();
764   }
765 
getActivityManager()766   private ActivityManager getActivityManager() {
767     return (ActivityManager) realActivity.getSystemService(Context.ACTIVITY_SERVICE);
768   }
769 
770   /**
771    * Gets the last permission request submitted to this activity.
772    *
773    * @return The permission request details.
774    */
getLastRequestedPermission()775   public PermissionsRequest getLastRequestedPermission() {
776     return lastRequestedPermission;
777   }
778 
779   private final class ActivityInvoker {
780     private Method method;
781 
call(final String methodName, final Class... argumentClasses)782     public ActivityInvoker call(final String methodName, final Class... argumentClasses) {
783       try {
784         method = Activity.class.getDeclaredMethod(methodName, argumentClasses);
785         method.setAccessible(true);
786         return this;
787       } catch (NoSuchMethodException e) {
788         throw new RuntimeException(e);
789       }
790     }
791 
withNothing()792     public Object withNothing() {
793       return with();
794     }
795 
with(final Object... parameters)796     public Object with(final Object... parameters) {
797       try {
798         return method.invoke(realActivity, parameters);
799       } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
800         throw new RuntimeException(e);
801       }
802     }
803   }
804 
805   /** Class to hold a permissions request, including its request code. */
806   public static class PermissionsRequest {
807     public final int requestCode;
808     public final String[] requestedPermissions;
809 
PermissionsRequest(String[] requestedPermissions, int requestCode)810     public PermissionsRequest(String[] requestedPermissions, int requestCode) {
811       this.requestedPermissions = requestedPermissions;
812       this.requestCode = requestCode;
813     }
814   }
815 }
816