Home | History | Annotate | Download | only in controller
      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 
     30   public static <T extends Activity> ActivityController<T> of(T activity, Intent intent) {
     31     return new ActivityController<>(activity, intent).attach();
     32   }
     33 
     34   public static <T extends Activity> ActivityController<T> of(T activity) {
     35     return new ActivityController<>(activity, null).attach();
     36   }
     37 
     38   private ActivityController(T activity, Intent intent) {
     39     super(activity, intent);
     40   }
     41 
     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 
     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 
     68   public ActivityController<T> create(final Bundle bundle) {
     69     shadowMainLooper.runPaused(() -> getInstrumentation().callActivityOnCreate(component, bundle));
     70     return this;
     71   }
     72 
     73   @Override public ActivityController<T> create() {
     74     return create(null);
     75   }
     76 
     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 
     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 
    100   public ActivityController<T> restoreInstanceState(Bundle bundle) {
    101     shadowMainLooper.runPaused(
    102         () -> getInstrumentation().callActivityOnRestoreInstanceState(component, bundle));
    103     return this;
    104   }
    105 
    106   public ActivityController<T> postCreate(Bundle bundle) {
    107     invokeWhilePaused("onPostCreate", from(Bundle.class, bundle));
    108     return this;
    109   }
    110 
    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 
    122   public ActivityController<T> postResume() {
    123     invokeWhilePaused("onPostResume");
    124     return this;
    125   }
    126 
    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 
    145   private ViewRootImpl getViewRoot() {
    146     return component.getWindow().getDecorView().getViewRootImpl();
    147   }
    148 
    149   private void callDispatchResized(ViewRootImpl root) {
    150     ((ShadowViewRootImpl) extract(root)).callDispatchResized();
    151   }
    152 
    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 
    169   public ActivityController<T> userLeaving() {
    170     shadowMainLooper.runPaused(() -> getInstrumentation().callActivityOnUserLeaving(component));
    171     return this;
    172   }
    173 
    174   public ActivityController<T> pause() {
    175     shadowMainLooper.runPaused(() -> getInstrumentation().callActivityOnPause(component));
    176     return this;
    177   }
    178 
    179   public ActivityController<T> saveInstanceState(Bundle outState) {
    180     shadowMainLooper.runPaused(
    181         () -> getInstrumentation().callActivityOnSaveInstanceState(component, outState));
    182     return this;
    183   }
    184 
    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 
    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    */
    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    */
    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 
    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    */
    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    */
    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 
    395   private static Instrumentation getInstrumentation() {
    396     return ((ActivityThread) RuntimeEnvironment.getActivityThread()).getInstrumentation();
    397   }
    398 }
    399