1 package org.robolectric.android.controller; 2 3 import static android.os.Build.VERSION_CODES.M; 4 import static android.os.Build.VERSION_CODES.O_MR1; 5 import static com.google.common.base.Preconditions.checkNotNull; 6 import static org.robolectric.shadow.api.Shadow.extract; 7 import static org.robolectric.util.ReflectionHelpers.ClassParameter.from; 8 9 import android.app.Activity; 10 import android.app.ActivityThread; 11 import android.app.Application; 12 import android.app.Instrumentation; 13 import android.content.ComponentName; 14 import android.content.Context; 15 import android.content.Intent; 16 import android.content.pm.ActivityInfo; 17 import android.content.pm.PackageManager; 18 import android.content.res.Configuration; 19 import android.os.Bundle; 20 import android.view.ViewRootImpl; 21 import org.robolectric.RuntimeEnvironment; 22 import org.robolectric.shadow.api.Shadow; 23 import org.robolectric.shadows.ShadowActivity; 24 import org.robolectric.shadows.ShadowContextThemeWrapper; 25 import org.robolectric.shadows.ShadowViewRootImpl; 26 import org.robolectric.util.ReflectionHelpers; 27 28 public class ActivityController<T extends Activity> extends ComponentController<ActivityController<T>, T> { 29 of(T activity, Intent intent)30 public static <T extends Activity> ActivityController<T> of(T activity, Intent intent) { 31 return new ActivityController<>(activity, intent).attach(); 32 } 33 of(T activity)34 public static <T extends Activity> ActivityController<T> of(T activity) { 35 return new ActivityController<>(activity, null).attach(); 36 } 37 ActivityController(T activity, Intent intent)38 private ActivityController(T activity, Intent intent) { 39 super(activity, intent); 40 } 41 attach()42 private ActivityController<T> attach() { 43 if (attached) { 44 return this; 45 } 46 // make sure the component is enabled 47 Context context = RuntimeEnvironment.application.getBaseContext(); 48 context 49 .getPackageManager() 50 .setComponentEnabledSetting( 51 new ComponentName(context.getPackageName(), component.getClass().getName()), 52 PackageManager.COMPONENT_ENABLED_STATE_ENABLED, 53 0); 54 ShadowActivity shadowActivity = Shadow.extract(component); 55 shadowActivity.callAttach(getIntent()); 56 attached = true; 57 return this; 58 } 59 getActivityInfo(Application application)60 private ActivityInfo getActivityInfo(Application application) { 61 try { 62 return application.getPackageManager().getActivityInfo(new ComponentName(application.getPackageName(), component.getClass().getName()), PackageManager.GET_ACTIVITIES | PackageManager.GET_META_DATA); 63 } catch (PackageManager.NameNotFoundException e) { 64 throw new RuntimeException(e); 65 } 66 } 67 create(final Bundle bundle)68 public ActivityController<T> create(final Bundle bundle) { 69 shadowMainLooper.runPaused(() -> getInstrumentation().callActivityOnCreate(component, bundle)); 70 return this; 71 } 72 create()73 @Override public ActivityController<T> create() { 74 return create(null); 75 } 76 restart()77 public ActivityController<T> restart() { 78 if (RuntimeEnvironment.getApiLevel() <= O_MR1) { 79 invokeWhilePaused("performRestart"); 80 } else { 81 invokeWhilePaused("performRestart", 82 from(boolean.class, true), 83 from(String.class, "restart()")); 84 } 85 return this; 86 } 87 start()88 public ActivityController<T> start() { 89 // Start and stop are tricky cases. Unlike other lifecycle methods such as 90 // Instrumentation#callActivityOnPause calls Activity#performPause, Activity#performStop calls 91 // Instrumentation#callActivityOnStop internally so the dependency direction is the opposite. 92 if (RuntimeEnvironment.getApiLevel() <= O_MR1) { 93 invokeWhilePaused("performStart"); 94 } else { 95 invokeWhilePaused("performStart", from(String.class, "start()")); 96 } 97 return this; 98 } 99 restoreInstanceState(Bundle bundle)100 public ActivityController<T> restoreInstanceState(Bundle bundle) { 101 shadowMainLooper.runPaused( 102 () -> getInstrumentation().callActivityOnRestoreInstanceState(component, bundle)); 103 return this; 104 } 105 postCreate(Bundle bundle)106 public ActivityController<T> postCreate(Bundle bundle) { 107 invokeWhilePaused("onPostCreate", from(Bundle.class, bundle)); 108 return this; 109 } 110 resume()111 public ActivityController<T> resume() { 112 if (RuntimeEnvironment.getApiLevel() <= O_MR1) { 113 invokeWhilePaused("performResume"); 114 } else { 115 invokeWhilePaused("performResume", 116 from(boolean.class, true), 117 from(String.class, "resume()")); 118 } 119 return this; 120 } 121 postResume()122 public ActivityController<T> postResume() { 123 invokeWhilePaused("onPostResume"); 124 return this; 125 } 126 visible()127 public ActivityController<T> visible() { 128 shadowMainLooper.runPaused(new Runnable() { 129 @Override 130 public void run() { 131 ReflectionHelpers.setField(component, "mDecor", component.getWindow().getDecorView()); 132 ReflectionHelpers.callInstanceMethod(component, "makeVisible"); 133 } 134 }); 135 136 ViewRootImpl root = getViewRoot(); 137 // root can be null if activity does not have content attached, or if looper is paused. 138 // this is unusual but leave the check here for legacy compatibility 139 if (root != null) { 140 callDispatchResized(root); 141 } 142 return this; 143 } 144 getViewRoot()145 private ViewRootImpl getViewRoot() { 146 return component.getWindow().getDecorView().getViewRootImpl(); 147 } 148 callDispatchResized(ViewRootImpl root)149 private void callDispatchResized(ViewRootImpl root) { 150 ((ShadowViewRootImpl) extract(root)).callDispatchResized(); 151 } 152 windowFocusChanged(boolean hasFocus)153 public ActivityController<T> windowFocusChanged(boolean hasFocus) { 154 ViewRootImpl root = getViewRoot(); 155 if (root == null) { 156 // root can be null if looper was paused during visible. Flush the looper and try again 157 shadowMainLooper.idle(); 158 159 root = checkNotNull(getViewRoot()); 160 callDispatchResized(root); 161 } 162 163 ReflectionHelpers.callInstanceMethod(root, "windowFocusChanged", 164 from(boolean.class, hasFocus), /* hasFocus */ 165 from(boolean.class, false) /* inTouchMode */); 166 return this; 167 } 168 userLeaving()169 public ActivityController<T> userLeaving() { 170 shadowMainLooper.runPaused(() -> getInstrumentation().callActivityOnUserLeaving(component)); 171 return this; 172 } 173 pause()174 public ActivityController<T> pause() { 175 shadowMainLooper.runPaused(() -> getInstrumentation().callActivityOnPause(component)); 176 return this; 177 } 178 saveInstanceState(Bundle outState)179 public ActivityController<T> saveInstanceState(Bundle outState) { 180 shadowMainLooper.runPaused( 181 () -> getInstrumentation().callActivityOnSaveInstanceState(component, outState)); 182 return this; 183 } 184 stop()185 public ActivityController<T> stop() { 186 // Stop and start are tricky cases. Unlike other lifecycle methods such as 187 // Instrumentation#callActivityOnPause calls Activity#performPause, Activity#performStop calls 188 // Instrumentation#callActivityOnStop internally so the dependency direction is the opposite. 189 if (RuntimeEnvironment.getApiLevel() <= M) { 190 invokeWhilePaused("performStop"); 191 } else if (RuntimeEnvironment.getApiLevel() <= O_MR1) { 192 invokeWhilePaused("performStop", from(boolean.class, true)); 193 } else { 194 invokeWhilePaused("performStop", from(boolean.class, true), from(String.class, "stop()")); 195 } 196 return this; 197 } 198 destroy()199 @Override public ActivityController<T> destroy() { 200 shadowMainLooper.runPaused(() -> getInstrumentation().callActivityOnDestroy(component)); 201 return this; 202 } 203 204 /** 205 * Calls the same lifecycle methods on the Activity called by Android the first time the Activity is created. 206 * 207 * @return Activity controller instance. 208 */ setup()209 public ActivityController<T> setup() { 210 return create().start().postCreate(null).resume().visible(); 211 } 212 213 /** 214 * Calls the same lifecycle methods on the Activity called by Android when an Activity is restored from previously saved state. 215 * 216 * @param savedInstanceState Saved instance state. 217 * @return Activity controller instance. 218 */ setup(Bundle savedInstanceState)219 public ActivityController<T> setup(Bundle savedInstanceState) { 220 return create(savedInstanceState) 221 .start() 222 .restoreInstanceState(savedInstanceState) 223 .postCreate(savedInstanceState) 224 .resume() 225 .visible(); 226 } 227 newIntent(Intent intent)228 public ActivityController<T> newIntent(Intent intent) { 229 invokeWhilePaused("onNewIntent", from(Intent.class, intent)); 230 return this; 231 } 232 233 /** 234 * Applies the current system configuration to the Activity. 235 * 236 * This can be used in conjunction with {@link RuntimeEnvironment#setQualifiers(String)} to 237 * simulate configuration changes. 238 * 239 * If the activity is configured to handle changes without being recreated, 240 * {@link Activity#onConfigurationChanged(Configuration)} will be called. Otherwise, the activity 241 * is recreated as described [here](https://developer.android.com/guide/topics/resources/runtime-changes.html). 242 * 243 * @return ActivityController instance 244 */ configurationChange()245 public ActivityController<T> configurationChange() { 246 return configurationChange(component.getApplicationContext().getResources().getConfiguration()); 247 } 248 249 /** 250 * Performs a configuration change on the Activity. 251 * 252 * If the activity is configured to handle changes without being recreated, 253 * {@link Activity#onConfigurationChanged(Configuration)} will be called. Otherwise, the activity 254 * is recreated as described [here](https://developer.android.com/guide/topics/resources/runtime-changes.html). 255 * 256 * @param newConfiguration The new configuration to be set. 257 * @return ActivityController instance 258 */ configurationChange(final Configuration newConfiguration)259 public ActivityController<T> configurationChange(final Configuration newConfiguration) { 260 final Configuration currentConfig = component.getResources().getConfiguration(); 261 final int changedBits = currentConfig.diff(newConfiguration); 262 currentConfig.setTo(newConfiguration); 263 264 // TODO: throw on changedBits == 0 since it non-intuitively calls onConfigurationChanged 265 266 // Can the activity handle itself ALL configuration changes? 267 if ((getActivityInfo(component.getApplication()).configChanges & changedBits) == changedBits) { 268 shadowMainLooper.runPaused(new Runnable() { 269 @Override 270 public void run() { 271 ReflectionHelpers.callInstanceMethod(Activity.class, component, "onConfigurationChanged", 272 from(Configuration.class, newConfiguration)); 273 } 274 }); 275 276 return this; 277 } else { 278 @SuppressWarnings("unchecked") 279 final T recreatedActivity = (T) ReflectionHelpers.callConstructor(component.getClass()); 280 281 shadowMainLooper.runPaused( 282 new Runnable() { 283 @Override 284 public void run() { 285 // Set flags 286 ReflectionHelpers.setField( 287 Activity.class, component, "mChangingConfigurations", true); 288 ReflectionHelpers.setField( 289 Activity.class, component, "mConfigChangeFlags", changedBits); 290 291 // Perform activity destruction 292 final Bundle outState = new Bundle(); 293 294 // The order of onPause/onStop/onSaveInstanceState is undefined, but is usually: 295 // onPause -> onSaveInstanceState -> onStop 296 ReflectionHelpers.callInstanceMethod(Activity.class, component, "performPause"); 297 ReflectionHelpers.callInstanceMethod( 298 Activity.class, 299 component, 300 "performSaveInstanceState", 301 from(Bundle.class, outState)); 302 if (RuntimeEnvironment.getApiLevel() <= M) { 303 ReflectionHelpers.callInstanceMethod(Activity.class, component, "performStop"); 304 } else if (RuntimeEnvironment.getApiLevel() <= O_MR1) { 305 ReflectionHelpers.callInstanceMethod( 306 Activity.class, component, "performStop", from(boolean.class, true)); 307 } else { 308 ReflectionHelpers.callInstanceMethod( 309 Activity.class, 310 component, 311 "performStop", 312 from(boolean.class, true), 313 from(String.class, "configurationChange")); 314 } 315 316 // This is the true and complete retained state, including loaders and retained 317 // fragments. 318 final Object nonConfigInstance = 319 ReflectionHelpers.callInstanceMethod( 320 Activity.class, component, "retainNonConfigurationInstances"); 321 // This is the activity's "user" state 322 final Object activityConfigInstance = 323 nonConfigInstance == null 324 ? null // No framework or user state. 325 : ReflectionHelpers.getField(nonConfigInstance, "activity"); 326 327 ReflectionHelpers.callInstanceMethod(Activity.class, component, "performDestroy"); 328 329 // Restore theme in case it was set in the test manually. 330 // This is not technically what happens but is purely to make this easier to use in 331 // Robolectric. 332 ShadowContextThemeWrapper shadowContextThemeWrapper = Shadow.extract(component); 333 int theme = shadowContextThemeWrapper.callGetThemeResId(); 334 335 // Setup controller for the new activity 336 attached = false; 337 component = recreatedActivity; 338 attach(); 339 340 if (theme != 0) { 341 recreatedActivity.setTheme(theme); 342 } 343 344 // Set saved non config instance 345 ReflectionHelpers.setField( 346 recreatedActivity, "mLastNonConfigurationInstances", nonConfigInstance); 347 ShadowActivity shadowActivity = Shadow.extract(recreatedActivity); 348 shadowActivity.setLastNonConfigurationInstance(activityConfigInstance); 349 350 // Create lifecycle 351 ReflectionHelpers.callInstanceMethod( 352 Activity.class, recreatedActivity, "performCreate", from(Bundle.class, outState)); 353 354 if (RuntimeEnvironment.getApiLevel() <= O_MR1) { 355 356 ReflectionHelpers.callInstanceMethod( 357 Activity.class, recreatedActivity, "performStart"); 358 359 } else { 360 ReflectionHelpers.callInstanceMethod( 361 Activity.class, 362 recreatedActivity, 363 "performStart", 364 from(String.class, "configurationChange")); 365 } 366 367 ReflectionHelpers.callInstanceMethod( 368 Activity.class, 369 recreatedActivity, 370 "performRestoreInstanceState", 371 from(Bundle.class, outState)); 372 ReflectionHelpers.callInstanceMethod( 373 Activity.class, recreatedActivity, "onPostCreate", from(Bundle.class, outState)); 374 if (RuntimeEnvironment.getApiLevel() <= O_MR1) { 375 ReflectionHelpers.callInstanceMethod( 376 Activity.class, recreatedActivity, "performResume"); 377 } else { 378 ReflectionHelpers.callInstanceMethod( 379 Activity.class, 380 recreatedActivity, 381 "performResume", 382 from(boolean.class, true), 383 from(String.class, "configurationChange")); 384 } 385 ReflectionHelpers.callInstanceMethod( 386 Activity.class, recreatedActivity, "onPostResume"); 387 // TODO: Call visible() too. 388 } 389 }); 390 } 391 392 return this; 393 } 394 getInstrumentation()395 private static Instrumentation getInstrumentation() { 396 return ((ActivityThread) RuntimeEnvironment.getActivityThread()).getInstrumentation(); 397 } 398 } 399