1 package org.robolectric.android.internal;
2 
3 import static android.os.Build.VERSION_CODES.O;
4 import static org.robolectric.Shadows.shadowOf;
5 import static org.robolectric.shadow.api.Shadow.extract;
6 
7 import android.app.Activity;
8 import android.app.Application;
9 import android.app.Instrumentation;
10 import android.content.Context;
11 import android.content.Intent;
12 import android.content.pm.ActivityInfo;
13 import android.content.res.Configuration;
14 import android.content.res.Resources;
15 import android.os.Bundle;
16 import android.os.Handler;
17 import android.os.IBinder;
18 import android.os.Looper;
19 import android.os.UserHandle;
20 import android.util.DisplayMetrics;
21 import android.util.Log;
22 import androidx.test.internal.runner.intent.IntentMonitorImpl;
23 import androidx.test.internal.runner.lifecycle.ActivityLifecycleMonitorImpl;
24 import androidx.test.internal.runner.lifecycle.ApplicationLifecycleMonitorImpl;
25 import androidx.test.platform.app.InstrumentationRegistry;
26 import androidx.test.runner.intent.IntentMonitorRegistry;
27 import androidx.test.runner.intent.IntentStubberRegistry;
28 import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry;
29 import androidx.test.runner.lifecycle.ApplicationLifecycleMonitorRegistry;
30 import androidx.test.runner.lifecycle.ApplicationStage;
31 import androidx.test.runner.lifecycle.Stage;
32 import java.util.ArrayList;
33 import java.util.List;
34 import java.util.Map;
35 import java.util.Set;
36 import java.util.concurrent.Callable;
37 import java.util.concurrent.ExecutionException;
38 import java.util.concurrent.FutureTask;
39 import java.util.concurrent.atomic.AtomicBoolean;
40 import java.util.concurrent.atomic.AtomicReference;
41 import javax.annotation.Nullable;
42 import org.robolectric.Robolectric;
43 import org.robolectric.RuntimeEnvironment;
44 import org.robolectric.android.controller.ActivityController;
45 import org.robolectric.annotation.LooperMode;
46 import org.robolectric.shadow.api.Shadow;
47 import org.robolectric.shadows.ShadowActivity;
48 import org.robolectric.shadows.ShadowInstrumentation;
49 import org.robolectric.shadows.ShadowLooper;
50 import org.robolectric.shadows.ShadowPausedLooper;
51 
52 /**
53  * A Robolectric instrumentation that acts like a slimmed down {@link
54  * androidx.test.runner.MonitoringInstrumentation} with only the parts needed for Robolectric.
55  */
56 @SuppressWarnings("RestrictTo")
57 public class RoboMonitoringInstrumentation extends Instrumentation {
58 
59   private static final String TAG = "RoboInstrumentation";
60 
61   private final ActivityLifecycleMonitorImpl lifecycleMonitor = new ActivityLifecycleMonitorImpl();
62   private final ApplicationLifecycleMonitorImpl applicationMonitor =
63       new ApplicationLifecycleMonitorImpl();
64   private final IntentMonitorImpl intentMonitor = new IntentMonitorImpl();
65   private final List<ActivityController<?>> createdActivities = new ArrayList<>();
66 
67   private final AtomicBoolean attachedConfigListener = new AtomicBoolean();
68 
69   /**
70    * Sets up lifecycle monitoring, and argument registry.
71    *
72    * <p>Subclasses must call up to onCreate(). This onCreate method does not call start() it is the
73    * subclasses responsibility to call start if it desires.
74    */
75   @Override
onCreate(Bundle arguments)76   public void onCreate(Bundle arguments) {
77     InstrumentationRegistry.registerInstance(this, arguments);
78     ActivityLifecycleMonitorRegistry.registerInstance(lifecycleMonitor);
79     ApplicationLifecycleMonitorRegistry.registerInstance(applicationMonitor);
80     IntentMonitorRegistry.registerInstance(intentMonitor);
81     super.onCreate(arguments);
82   }
83 
84   @Override
waitForIdleSync()85   public void waitForIdleSync() {
86     shadowOf(Looper.getMainLooper()).idle();
87   }
88 
89   @Override
startActivitySync(final Intent intent)90   public Activity startActivitySync(final Intent intent) {
91     return startActivitySyncInternal(intent).get();
92   }
93 
startActivitySyncInternal(Intent intent)94   public ActivityController<? extends Activity> startActivitySyncInternal(Intent intent) {
95     return startActivitySyncInternal(intent, /* activityOptions= */ null);
96   }
97 
startActivitySyncInternal( Intent intent, @Nullable Bundle activityOptions)98   public ActivityController<? extends Activity> startActivitySyncInternal(
99       Intent intent, @Nullable Bundle activityOptions) {
100     ActivityInfo ai = intent.resolveActivityInfo(getTargetContext().getPackageManager(), 0);
101     if (ai == null) {
102       throw new RuntimeException(
103           "Unable to resolve activity for "
104               + intent
105               + " -- see https://github.com/robolectric/robolectric/pull/4736 for details");
106     }
107 
108     Class<? extends Activity> activityClass;
109     String activityClassName = ai.targetActivity != null ? ai.targetActivity : ai.name;
110     try {
111       activityClass = Class.forName(activityClassName).asSubclass(Activity.class);
112     } catch (ClassNotFoundException e) {
113       throw new RuntimeException("Could not load activity " + ai.name, e);
114     }
115 
116     if (attachedConfigListener.compareAndSet(false, true) && !willCreateActivityContexts()) {
117       // To avoid infinite recursion listen to the system resources, this will be updated before
118       // the application resources but because activities use the application resources they will
119       // get updated by the first activity (via updateConfiguration).
120       shadowOf(Resources.getSystem()).addConfigurationChangeListener(this::updateConfiguration);
121     }
122 
123     AtomicReference<ActivityController<? extends Activity>> activityControllerReference =
124         new AtomicReference<>();
125     ShadowInstrumentation.runOnMainSyncNoIdle(
126         () -> {
127           ActivityController<? extends Activity> controller =
128               Robolectric.buildActivity(activityClass, intent, activityOptions);
129           activityControllerReference.set(controller);
130           controller.create();
131           if (controller.get().isFinishing()) {
132             controller.destroy();
133           } else {
134             createdActivities.add(controller);
135             controller
136                 .start()
137                 .postCreate(null)
138                 .resume()
139                 .visible()
140                 .windowFocusChanged(true)
141                 .topActivityResumed(true);
142           }
143         });
144     return activityControllerReference.get();
145   }
146 
147   @Override
callApplicationOnCreate(Application app)148   public void callApplicationOnCreate(Application app) {
149     if (willCreateActivityContexts()) {
150       shadowOf(app.getResources()).addConfigurationChangeListener(this::updateConfiguration);
151     }
152     applicationMonitor.signalLifecycleChange(app, ApplicationStage.PRE_ON_CREATE);
153     super.callApplicationOnCreate(app);
154     applicationMonitor.signalLifecycleChange(app, ApplicationStage.CREATED);
155   }
156 
157   /**
158    * Executes a runnable on the main thread, blocking until it is complete.
159    *
160    * <p>When in INSTUMENTATION_TEST Looper mode, the runnable is posted to the main handler and the
161    * caller's thread blocks until that runnable has finished. When a Throwable is thrown in the
162    * runnable, the exception is propagated back to the caller's thread. If it is an unchecked
163    * throwable, it will be rethrown as is. If it is a checked exception, it will be rethrown as a
164    * {@link RuntimeException}.
165    *
166    * <p>For other Looper modes, the main looper is idled and then the runnable is executed in the
167    * caller's thread.
168    *
169    * @param runnable a runnable to be executed on the main thread
170    */
171   @Override
runOnMainSync(Runnable runnable)172   public void runOnMainSync(Runnable runnable) {
173     if (ShadowLooper.looperMode() == LooperMode.Mode.INSTRUMENTATION_TEST) {
174       FutureTask<Void> wrapped = new FutureTask<>(runnable, null);
175       Shadow.<ShadowPausedLooper>extract(Looper.getMainLooper()).postSync(wrapped);
176       try {
177         wrapped.get();
178       } catch (InterruptedException e) {
179         throw new RuntimeException(e);
180       } catch (ExecutionException e) {
181         Throwable cause = e.getCause();
182         if (cause instanceof RuntimeException) {
183           throw (RuntimeException) cause;
184         } else if (cause instanceof Error) {
185           throw (Error) cause;
186         }
187         throw new RuntimeException(cause);
188       }
189     } else {
190       // TODO: Use ShadowPausedLooper#postSync for PAUSED looper mode which provides more realistic
191       //  behavior (i.e. it only runs to the runnable, it doesn't completely idle).
192       waitForIdleSync();
193       runnable.run();
194     }
195   }
196 
197   /** {@inheritDoc} */
198   @Override
execStartActivity( Context who, IBinder contextThread, IBinder token, Activity target, Intent intent, int requestCode, Bundle options)199   public ActivityResult execStartActivity(
200       Context who,
201       IBinder contextThread,
202       IBinder token,
203       Activity target,
204       Intent intent,
205       int requestCode,
206       Bundle options) {
207     intentMonitor.signalIntent(intent);
208     ActivityResult ar = stubResultFor(intent);
209     if (ar != null) {
210       Log.i(TAG, String.format("Stubbing intent %s", intent));
211     } else {
212       ar = super.execStartActivity(who, contextThread, token, target, intent, requestCode, options);
213     }
214     if (ar != null && target != null) {
215       ShadowActivity shadowActivity = extract(target);
216       postDispatchActivityResult(shadowActivity, null, requestCode, ar);
217     }
218     return null;
219   }
220 
221   /** This API was added in Android API 23 (M) */
222   @Override
execStartActivity( Context who, IBinder contextThread, IBinder token, String target, Intent intent, int requestCode, Bundle options)223   public ActivityResult execStartActivity(
224       Context who,
225       IBinder contextThread,
226       IBinder token,
227       String target,
228       Intent intent,
229       int requestCode,
230       Bundle options) {
231     intentMonitor.signalIntent(intent);
232     ActivityResult ar = stubResultFor(intent);
233     if (ar != null) {
234       Log.i(TAG, String.format("Stubbing intent %s", intent));
235     } else {
236       ar = super.execStartActivity(who, contextThread, token, target, intent, requestCode, options);
237     }
238     if (ar != null && who instanceof Activity) {
239       ShadowActivity shadowActivity = extract(who);
240       postDispatchActivityResult(shadowActivity, target, requestCode, ar);
241     }
242     return null;
243   }
244 
245   /** This API was added in Android API 17 (JELLY_BEAN_MR1) */
246   @Override
execStartActivity( Context who, IBinder contextThread, IBinder token, String target, Intent intent, int requestCode, Bundle options, UserHandle user)247   public ActivityResult execStartActivity(
248       Context who,
249       IBinder contextThread,
250       IBinder token,
251       String target,
252       Intent intent,
253       int requestCode,
254       Bundle options,
255       UserHandle user) {
256     ActivityResult ar = stubResultFor(intent);
257     if (ar != null) {
258       Log.i(TAG, String.format("Stubbing intent %s", intent));
259     } else {
260       ar =
261           super.execStartActivity(
262               who, contextThread, token, target, intent, requestCode, options, user);
263     }
264     if (ar != null && target != null) {
265       ShadowActivity shadowActivity = extract(target);
266       postDispatchActivityResult(shadowActivity, null, requestCode, ar);
267     }
268     return null;
269   }
270 
postDispatchActivityResult( ShadowActivity shadowActivity, String target, int requestCode, ActivityResult ar)271   private void postDispatchActivityResult(
272       ShadowActivity shadowActivity, String target, int requestCode, ActivityResult ar) {
273     new Handler(Looper.getMainLooper())
274         .post(
275             new Runnable() {
276               @Override
277               public void run() {
278                 shadowActivity.internalCallDispatchActivityResult(
279                     target, requestCode, ar.getResultCode(), ar.getResultData());
280               }
281             });
282   }
283 
stubResultFor(Intent intent)284   private ActivityResult stubResultFor(Intent intent) {
285     if (!IntentStubberRegistry.isLoaded()) {
286       return null;
287     }
288 
289     FutureTask<ActivityResult> task =
290         new FutureTask<ActivityResult>(
291             new Callable<ActivityResult>() {
292               @Override
293               public ActivityResult call() throws Exception {
294                 return IntentStubberRegistry.getInstance().getActivityResultForIntent(intent);
295               }
296             });
297     ShadowInstrumentation.runOnMainSyncNoIdle(task);
298 
299     try {
300       return task.get();
301     } catch (ExecutionException e) {
302       String msg = String.format("Could not retrieve stub result for intent %s", intent);
303       // Preserve original exception
304       if (e.getCause() instanceof RuntimeException) {
305         Log.w(TAG, msg, e);
306         throw (RuntimeException) e.getCause();
307       } else if (e.getCause() != null) {
308         throw new RuntimeException(msg, e.getCause());
309       } else {
310         throw new RuntimeException(msg, e);
311       }
312     } catch (InterruptedException e) {
313       throw new RuntimeException(e);
314     }
315   }
316 
317   /** {@inheritDoc} */
318   @Override
execStartActivities( Context who, IBinder contextThread, IBinder token, Activity target, Intent[] intents, Bundle options)319   public void execStartActivities(
320       Context who,
321       IBinder contextThread,
322       IBinder token,
323       Activity target,
324       Intent[] intents,
325       Bundle options) {
326     Log.d(TAG, "execStartActivities(context, ibinder, ibinder, activity, intent[], bundle)");
327     // For requestCode < 0, the caller doesn't expect any result and
328     // in this case we are not expecting any result so selecting
329     // a value < 0.
330     int requestCode = -1;
331     for (Intent intent : intents) {
332       execStartActivity(who, contextThread, token, target, intent, requestCode, options);
333     }
334   }
335 
336   @Override
onException(Object obj, Throwable e)337   public boolean onException(Object obj, Throwable e) {
338     String error =
339         String.format(
340             "Exception encountered by: %s. Dumping thread state to "
341                 + "outputs and pining for the fjords.",
342             obj);
343     Log.e(TAG, error, e);
344     Log.e("THREAD_STATE", getThreadState());
345     Log.e(TAG, "Dying now...");
346     return super.onException(obj, e);
347   }
348 
getThreadState()349   protected String getThreadState() {
350     Set<Map.Entry<Thread, StackTraceElement[]>> threads = Thread.getAllStackTraces().entrySet();
351     StringBuilder threadState = new StringBuilder();
352     for (Map.Entry<Thread, StackTraceElement[]> threadAndStack : threads) {
353       StringBuilder threadMessage = new StringBuilder("  ").append(threadAndStack.getKey());
354       threadMessage.append("\n");
355       for (StackTraceElement ste : threadAndStack.getValue()) {
356         threadMessage.append(String.format("    %s%n", ste));
357       }
358       threadMessage.append("\n");
359       threadState.append(threadMessage);
360     }
361     return threadState.toString();
362   }
363 
364   @Override
callActivityOnDestroy(Activity activity)365   public void callActivityOnDestroy(Activity activity) {
366     if (activity.isFinishing()) {
367       createdActivities.removeIf(controller -> controller.get() == activity);
368     }
369     super.callActivityOnDestroy(activity);
370     lifecycleMonitor.signalLifecycleChange(Stage.DESTROYED, activity);
371   }
372 
373   @Override
callActivityOnRestart(Activity activity)374   public void callActivityOnRestart(Activity activity) {
375     super.callActivityOnRestart(activity);
376     lifecycleMonitor.signalLifecycleChange(Stage.RESTARTED, activity);
377   }
378 
379   @Override
callActivityOnCreate(Activity activity, Bundle bundle)380   public void callActivityOnCreate(Activity activity, Bundle bundle) {
381     lifecycleMonitor.signalLifecycleChange(Stage.PRE_ON_CREATE, activity);
382     super.callActivityOnCreate(activity, bundle);
383     lifecycleMonitor.signalLifecycleChange(Stage.CREATED, activity);
384   }
385 
386   @Override
callActivityOnStart(Activity activity)387   public void callActivityOnStart(Activity activity) {
388     super.callActivityOnStart(activity);
389     lifecycleMonitor.signalLifecycleChange(Stage.STARTED, activity);
390   }
391 
392   @Override
callActivityOnStop(Activity activity)393   public void callActivityOnStop(Activity activity) {
394     super.callActivityOnStop(activity);
395     lifecycleMonitor.signalLifecycleChange(Stage.STOPPED, activity);
396   }
397 
398   @Override
callActivityOnResume(Activity activity)399   public void callActivityOnResume(Activity activity) {
400     super.callActivityOnResume(activity);
401     lifecycleMonitor.signalLifecycleChange(Stage.RESUMED, activity);
402   }
403 
404   @Override
callActivityOnPause(Activity activity)405   public void callActivityOnPause(Activity activity) {
406     super.callActivityOnPause(activity);
407     lifecycleMonitor.signalLifecycleChange(Stage.PAUSED, activity);
408   }
409 
410   @Override
finish(int resultCode, Bundle bundle)411   public void finish(int resultCode, Bundle bundle) {}
412 
413   @Override
getTargetContext()414   public Context getTargetContext() {
415     return RuntimeEnvironment.getApplication();
416   }
417 
418   @Override
getContext()419   public Context getContext() {
420     return RuntimeEnvironment.getApplication();
421   }
422 
updateConfiguration( Configuration oldConfig, Configuration newConfig, DisplayMetrics newMetrics)423   private void updateConfiguration(
424       Configuration oldConfig, Configuration newConfig, DisplayMetrics newMetrics) {
425     int changedConfig = oldConfig.diff(newConfig);
426     List<ActivityController<?>> controllers = new ArrayList<>(createdActivities);
427     for (ActivityController<?> controller : controllers) {
428       if (createdActivities.contains(controller)) {
429         Activity activity = controller.get();
430         if (System.getProperty("robolectric.configurationChangeFix", "true").equals("true")) {
431           controller.configurationChange(newConfig, newMetrics);
432         } else {
433           controller.configurationChange(newConfig, newMetrics, changedConfig);
434         }
435         // If the activity is recreated then make the new activity visible, this should be done by
436         // configurationChange but there's a pre-existing TODO to address this and it will require
437         // more work to make it function correctly.
438         if (controller.get() != activity) {
439           controller.visible();
440         }
441       }
442     }
443   }
444 
willCreateActivityContexts()445   private static boolean willCreateActivityContexts() {
446     return RuntimeEnvironment.getApiLevel() >= O
447         && Boolean.getBoolean("robolectric.createActivityContexts");
448   }
449 }
450