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