content: /*
   * Copyright (C) 2014 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *     http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package androidx.test.rule;
  
  import static androidx.test.internal.util.Checks.checkNotNull;
  import static androidx.test.internal.util.Checks.checkState;
  
  import android.app.Activity;
  import android.app.Instrumentation;
  import android.app.Instrumentation.ActivityResult;
  import android.content.Intent;
  import android.os.Bundle;
  import android.os.Looper;
  import android.util.Log;
  import androidx.annotation.NonNull;
  import androidx.annotation.Nullable;
  import androidx.annotation.VisibleForTesting;
  import androidx.test.annotation.UiThreadTest;
  import androidx.test.internal.runner.junit4.statement.UiThreadStatement;
  import androidx.test.platform.app.InstrumentationRegistry;
  import androidx.test.runner.MonitoringInstrumentation;
  import androidx.test.runner.intercepting.SingleActivityFactory;
  import androidx.test.runner.lifecycle.ActivityLifecycleCallback;
  import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry;
  import androidx.test.runner.lifecycle.Stage;
  import java.lang.ref.WeakReference;
  import java.lang.reflect.Field;
  import org.junit.rules.TestRule;
  import org.junit.runner.Description;
  import org.junit.runners.model.Statement;
  
  /**
   * This rule provides functional testing of a single {@link Activity}. When {@code launchActivity}
   * is set to true in the constructor, the Activity under test will be launched before each test
   * annotated with {@link org.junit.Test} </a> and before methods annotated with {@link
   * org.junit.Before}, and it will be terminated after the test is completed and methods annotated
   * with {@link org.junit.After} are finished.
   *
   * <p>The Activity can be manually launched with {@link #launchActivity(Intent)}, and manually
   * finished with {@link #finishActivity()}. If the Activity is running at the end of the test, the
   * test rule will finish it.
   *
   * <p>During the duration of the test you will be able to manipulate your Activity directly using
   * the reference obtained from {@link #getActivity()}. If the Activity is finished and relaunched,
   * the reference returned by {@link #getActivity()} will always point to the current instance of the
   * Activity.
   *
   * @param <T> The Activity class under test
   * @deprecated use {@link androidx.test.core.app.ActivityScenario} or {@link
   *     androidx.test.ext.junit.rules.ActivityScenarioRule} instead. They offer a simpler, and safer
   *     way of controlling Activity lifecycles.
   *     <p>Here are some tips to consider when converting to <code>ActivityScenario/Rule</code>:
   *     <ol>
   *       <li>For simple cases where you want to launch the Activity before each test and tear it
   *           down after each test (eg you are using {@link #ActivityTestRule(Class)}), convert
   *           directly to ActivityScenarioRule.
   *       <li>If you need control over when to launch the Activity (eg you are using {@link
   *           #ActivityTestRule(Class, false, false)}), use ActivityScenario.launch. It's recommended
   *           to wrap the launch in a try-block, so the Activity is closed automatically. <code>
   *       try (ActivityScenario.launch(activityClass)) {
   *         ...
   *       }
   *     </code>
   *       <li>If you need access to the Activity during the test (eg you are calling {@link
   *           ActivityTestRule#getActivity()}) provide a Runnable callback to {@link
   *           androidx.test.core.app.ActivityScenario#onActivity(Runnable)} instead. The callback
   *           provided to onActivity will run on the application's main thread, thus ensuring a safer
   *           mechanism to access the Activity.
   *     </ol>
   */
  @Deprecated
  public class ActivityTestRule<T extends Activity> implements TestRule {
  
    private static final String TAG = "ActivityTestRule";
  
    private static final int NO_FLAGS_SET = 0;
    private static final String FIELD_RESULT_CODE = "mResultCode";
    private static final String FIELD_RESULT_DATA = "mResultData";
  
    private final Class<T> activityClass;
  
    private final String targetPackage;
  
    private final int launchFlags;
  
    private final ActivityLifecycleCallback lifecycleCallback = new LifecycleCallback();
  
    private Instrumentation instrumentation;
  
    private boolean initialTouchMode = false;
  
    private boolean launchActivity = false;
  
    private SingleActivityFactory<T> activityFactory;
  
    @VisibleForTesting volatile WeakReference<T> activity = makeWeakReference(null);
  
    private volatile ActivityResult activityResult;
  
    /**
     * Similar to {@link #ActivityTestRule(Class, boolean)} but with "touch mode" disabled.
     *
     * @param activityClass The activity under test. This must be a class in the instrumentation
     *     targetPackage specified in the AndroidManifest.xml
     * @see ActivityTestRule#ActivityTestRule(Class, boolean, boolean)
     */
    public ActivityTestRule(Class<T> activityClass) {
      this(activityClass, false);
    }
  
    /**
     * Similar to {@link #ActivityTestRule(Class, boolean, boolean)} but defaults to launch the
     * activity under test once per {@link org.junit.Test} method. It is launched before the first
     * {@link org.junit.Before} method, and terminated after the last {@link org.junit.After} method.
     *
     * @param activityClass The activity under test. This must be a class in the instrumentation
     *     targetPackage specified in the AndroidManifest.xml
     * @param initialTouchMode true if the Activity should be placed into "touch mode" when started
     * @see ActivityTestRule#ActivityTestRule(Class, boolean, boolean)
     */
    public ActivityTestRule(Class<T> activityClass, boolean initialTouchMode) {
      this(activityClass, initialTouchMode, true);
    }
  
    /**
     * Similar to {@link #ActivityTestRule(Class, String, int, boolean, boolean)} but defaults to
     * launch the Activity with the default target package name {@link
     * InstrumentationRegistry#getTargetContext()#getPackageName} and {@link
     * Intent#FLAG_ACTIVITY_NEW_TASK} launch flag.
     *
     * @param activityClass The activity under test. This must be a class in the instrumentation
     *     targetPackage specified in the AndroidManifest.xml
     * @param initialTouchMode true if the Activity should be placed into "touch mode" when started
     * @param launchActivity true if the Activity should be launched once per {@link org.junit.Test}
     *     method. It will be launched before the first {@link org.junit.Before} method, and
     *     terminated after the last {@link org.junit.After} method.
     */
    public ActivityTestRule(
        Class<T> activityClass, boolean initialTouchMode, boolean launchActivity) {
      this(
          activityClass,
          InstrumentationRegistry.getInstrumentation().getTargetContext().getPackageName(),
          Intent.FLAG_ACTIVITY_NEW_TASK,
          initialTouchMode,
          launchActivity);
    }
  
    /**
     * Creates an {@link ActivityTestRule} for the Activity under test.
     *
     * @param activityFactory factory to be used for creating Activity instance
     * @param initialTouchMode true if the Activity should be placed into "touch mode" when started
     * @param launchActivity true if the Activity should be launched once per {@link org.junit.Test}
     *     method. It will be launched before the first {@link org.junit.Before} method, and
     *     terminated after the last {@link org.junit.After} method.
     */
    public ActivityTestRule(
        SingleActivityFactory<T> activityFactory, boolean initialTouchMode, boolean launchActivity) {
      this(activityFactory.getActivityClassToIntercept(), initialTouchMode, launchActivity);
      this.activityFactory = activityFactory;
    }
  
    /**
     * Creates an {@link ActivityTestRule} for the Activity under test.
     *
     * @param activityClass The activity under test. This must be a class in the instrumentation
     *     targetPackage specified in the AndroidManifest.xml
     * @param initialTouchMode true if the Activity should be placed into "touch mode" when started
     * @param launchActivity true if the Activity should be launched once per {@link org.junit.Test}
     *     method. It will be launched before the first {@link org.junit.Before} method, and
     *     terminated after the last {@link org.junit.After} method.
     * @param targetPackage The name of the target package that the Activity is started under. This
     *     value is passed down to the start Intent using {@link
     *     Intent#setClassName(android.content.Context, String)}. Can not be null.
     * @param launchFlags launch flags to start the Activity under test.
     */
    public ActivityTestRule(
        Class<T> activityClass,
        @NonNull String targetPackage,
        int launchFlags,
        boolean initialTouchMode,
        boolean launchActivity) {
      instrumentation = InstrumentationRegistry.getInstrumentation();
      this.activityClass = activityClass;
      this.targetPackage = checkNotNull(targetPackage, "targetPackage cannot be null!");
      this.launchFlags = launchFlags;
      this.initialTouchMode = initialTouchMode;
      this.launchActivity = launchActivity;
    }
  
    /**
     * Override this method to set up a custom Intent as if supplied to {@link
     * android.content.Context#startActivity}. Custom Intents provided by this method will take
     * precedence over default Intents that where created in the constructor but be overridden by any
     * Intents passed in through {@link #launchActivity(Intent)}.
     *
     * <p>The default Intent (if this method returns null or is not overwritten) is: action = {@link
     * Intent#ACTION_MAIN} flags = {@link Intent#FLAG_ACTIVITY_NEW_TASK} All other intent fields are
     * null or empty.
     *
     * <p>If the custom Intent provided by this methods overrides any of the following fields:
     *
     * <ul>
     *   <li>componentName through {@link Intent#setClassName(String, String)}
     *   <li>launch flags through {@link Intent#setFlags(int)}
     * </ul>
     *
     * <p>These custom values will be used to start the Activity. However, if some of these values are
     * not set, the default values documented in {@link #ActivityTestRule(Class, String, int, boolean,
     * boolean)} are supplemented.
     *
     * @return The Intent as if supplied to {@link android.content.Context#startActivity}.
     */
    protected Intent getActivityIntent() {
      return null;
    }
  
    /**
     * Override this method to execute any code that should run before your {@link Activity} is
     * created and launched. This method is called before each test method, including any method
     * annotated with {@link org.junit.Before}.
     */
    protected void beforeActivityLaunched() {
      // empty by default
    }
  
    /**
     * Override this method to execute any code that should run after your {@link Activity} is
     * launched, but before any test code is run including any method annotated with {@link
     * org.junit.Before}.
     *
     * <p>Prefer {@link org.junit.Before} over this method. This method should usually not be
     * overwritten directly in tests and only be used by subclasses of ActivityTestRule to get
     * notified when the activity is created and visible but test runs.
     */
    protected void afterActivityLaunched() {
      // empty by default
    }
  
    /**
     * Override this method to execute any code that should run after the currently launched {@link
     * Activity} is finished. This method is called after each test method, including any method
     * annotated with {@link org.junit.After}.
     *
     * <p>Prefer {@link org.junit.After} over this method. This method should usually not be
     * overwritten directly in tests and only be used by subclasses of ActivityTestRule to get
     * notified when the activity is created and visible but test runs.
     */
    protected void afterActivityFinished() {
      // empty by default
    }
  
    /**
     * Returns the reference to the activity under test.
     *
     * <p>The reference to the activity is assigned during the initial creation of the acivity and for
     * every sinlge {@link Activity#OnResumed()} lifecycle change.
     *
     * <p><b>Note:</b> Lifecycle changes happen on the UI thread (not the instrumentation thread where
     * this test code usually executes). Thus, the return value may vary depending on timing.
     *
     * <p>For example, if the activity is finished and relaunched, the reference returned by this
     * method will point to the new instance of the activity assuming {@link Activity#OnResumed()} was
     * called prior to calling this method.
     *
     * <p>If the activity wasn't created yet or already finished, {@code null} will be returned.
     *
     * <p><b>Note:</b> The activity reference is stored in a weak reference which means if the
     * activity under test is detroyed (ex. back button was pressed) then the system no longer holds a
     * strong reference to the acitivty and this refernce may get garbage collected.
     */
    public T getActivity() {
      T hardActivityRef = activity.get();
      if (hardActivityRef == null) {
        Log.w(TAG, "Activity wasn't created yet or already stopped");
      }
      return hardActivityRef;
    }
  
    @Override
    public Statement apply(final Statement base, Description description) {
      return new ActivityStatement(base);
    }
  
    /**
     * Launches the Activity under test.
     *
     * <p>Don't call this method directly, unless you explicitly requested not to lazily launch the
     * Activity manually using the launchActivity flag in {@link #ActivityTestRule(Class, boolean,
     * boolean)}.
     *
     * <p>Usage:
     *
     * <pre>
     *    &#064;Test
     *    public void customIntentToStartActivity() {
     *        Intent intent = new Intent(Intent.ACTION_PICK);
     *        activity = mActivityRule.launchActivity(intent);
     *    }
     * </pre>
     *
     * Note: Custom start Intents provided through this method will take precedence over default
     * Intents that where created in the constructor and any Intent returned from {@link
     * #getActivityIntent()}. The same override rules documented in {@link #getActivityIntent()}
     * apply.
     *
     * @param startIntent The Intent that will be used to start the Activity under test. If {@code
     *     startIntent} is null, the Intent returned by {@link ActivityTestRule#getActivityIntent()}
     *     is used.
     * @return the Activity launched by this rule.
     */
    public T launchActivity(@Nullable Intent startIntent) {
      // set initial touch mode
      instrumentation.setInTouchMode(initialTouchMode);
  
      // inject custom intent, if provided
      if (null == startIntent) {
        startIntent = getActivityIntent();
        if (null == startIntent) {
          Log.w(
              TAG,
              "getActivityIntent() returned null using default: " + "Intent(Intent.ACTION_MAIN)");
          startIntent = new Intent(Intent.ACTION_MAIN);
        }
      }
  
      // Set target component if not set Intent
      if (null == startIntent.getComponent()) {
        startIntent.setClassName(targetPackage, activityClass.getName());
      }
  
      // Set launch flags where if not set Intent
      if (NO_FLAGS_SET == startIntent.getFlags()) {
        startIntent.addFlags(launchFlags);
      }
  
      beforeActivityLaunched();
      // The following cast is correct because the activity we're creating is of the same type as
      // the one passed in
      T hardActivityRef = activityClass.cast(instrumentation.startActivitySync(startIntent));
      activity = makeWeakReference(hardActivityRef);
  
      instrumentation.waitForIdleSync();
  
      if (hardActivityRef != null) {
        // Notify that Activity was successfully launched
        ActivityLifecycleMonitorRegistry.getInstance().addLifecycleCallback(lifecycleCallback);
        afterActivityLaunched();
      } else {
        // Log an error message to logcat/instrumentation, that the Activity failed to launch
        String errorMessage =
            String.format("Activity %s, failed to launch", startIntent.getComponent());
        Bundle bundle = new Bundle();
        bundle.putString(Instrumentation.REPORT_KEY_STREAMRESULT, TAG + " " + errorMessage);
        instrumentation.sendStatus(0, bundle);
        Log.e(TAG, errorMessage);
      }
  
      return hardActivityRef;
    }
  
    @VisibleForTesting
    void setInstrumentation(Instrumentation instrumentation) {
      this.instrumentation = checkNotNull(instrumentation, "instrumentation cannot be null!");
    }
  
    /**
     * Finishes the currently launched Activity.
     *
     * @throws IllegalStateException if the Activity is not running or failed to finish it.
     */
    public void finishActivity() {
      try {
        if (activity.get() != null) {
          callFinishOnMainSync();
        }
      } finally {
        activity = makeWeakReference(null);
        afterActivityFinished(); // TODO(b/72327935) move down to evaluate
      }
    }
  
    @VisibleForTesting
    void callFinishOnMainSync() {
      try {
        final T hardActivityRef = activity.get();
        runOnUiThread(
            () -> {
              checkState(
                  hardActivityRef != null,
                  "Activity was not launched. If you manually finished it, you must launch it"
                      + " again before finishing it. ");
              hardActivityRef.finish();
              // If there is an activity result we save it
              setActivityResultForActivity(hardActivityRef);
            });
        instrumentation.waitForIdleSync();
      } catch (Throwable throwable) {
        // Should never happen
        String msg = "Failed to execute activity.finish() on the main thread";
        Log.e(TAG, msg, throwable);
        throw new IllegalStateException(msg, throwable);
      }
    }
  
    /**
     * This method can be used to retrieve the {@link ActivityResult} of an Activity that has called
     * {@link Activity#setResult}. Usually, the result is handled in {@link Activity#onActivityResult}
     * of the parent Activity, that has called {@link Activity#startActivityForResult}.
     *
     * <p>This method must <b>not</b> be called before {@code Activity.finish} was called or after the
     * activity was already destroyed.
     *
     * <p>Note: This method assumes {@link Activity#setResult(int)} is called no later than in {@link
     * Activity#onPause()}.
     *
     * @return the {@link ActivityResult} that was set most recently
     * @throws IllegalStateException if the activity is not in finishing state.
     */
    public ActivityResult getActivityResult() {
      if (null == activityResult) {
        // This is required if users manually called .finish() on their activity instead of using
        // this.finishActivity(). Since .finish() is async there could be a case that our callback
        // wasn't called just yet.
        T hardActivityRef = activity.get();
        checkNotNull(hardActivityRef, "Activity wasn't created yet or already destroyed!");
        try {
          runOnUiThread(
              () -> {
                checkState(hardActivityRef.isFinishing(), "Activity is not finishing!");
                setActivityResultForActivity(hardActivityRef);
              });
        } catch (Throwable throwable) {
          throw new IllegalStateException(throwable);
        }
      }
      return activityResult;
    }
  
    @SuppressWarnings("DiscouragedPrivateApi")
    private void setActivityResultForActivity(final T activity) {
      checkState(Looper.myLooper() == Looper.getMainLooper(), "Must be called on the main thread!");
      checkNotNull(activity, "Activity wasn't created yet or already destroyed!");
  
      try {
        Field resultCodeField = Activity.class.getDeclaredField(FIELD_RESULT_CODE);
        resultCodeField.setAccessible(true);
  
        Field resultDataField = Activity.class.getDeclaredField(FIELD_RESULT_DATA);
        resultDataField.setAccessible(true);
  
        activityResult =
            new ActivityResult(
                (int) resultCodeField.get(activity), (Intent) resultDataField.get(activity));
      } catch (NoSuchFieldException e) {
        String msg =
            "Looks like the Android Activity class has changed its"
                + "private fields for mResultCode or mResultData. "
                + "Time to update the reflection code.";
        Log.e(TAG, msg, e);
        throw new RuntimeException(msg, e);
      } catch (IllegalAccessException e) {
        String msg = "Field mResultCode or mResultData is not accessible";
        Log.e(TAG, msg, e);
        throw new RuntimeException(msg, e);
      }
    }
  
    /**
     * Helper method for running part of a method on the UI thread, blocking until it is complete.
     *
     * <p>Note: In most cases it is simpler to annotate the test method with {@link UiThreadTest}.
     *
     * <p>Use this method if you need to switch in and out of the UI thread within your method.
     *
     * @param runnable runnable containing test code in the {@link Runnable#run()} method
     * @see androidx.test.annotation.UiThreadTest
     */
    public void runOnUiThread(final Runnable runnable) throws Throwable {
      UiThreadStatement.runOnUiThread(runnable);
    }
  
    /**
     * {@link org.junit.runners.model.Statement} that finishes the activity after the test was
     * executed
     */
    private class ActivityStatement extends Statement {
  
      private final Statement base;
  
      public ActivityStatement(Statement base) {
        this.base = base;
      }
  
      @Override
      public void evaluate() throws Throwable {
        MonitoringInstrumentation instrumentation =
            ActivityTestRule.this.instrumentation instanceof MonitoringInstrumentation
                ? (MonitoringInstrumentation) ActivityTestRule.this.instrumentation
                : null;
        try {
          if (activityFactory != null && instrumentation != null) {
            instrumentation.interceptActivityUsing(activityFactory);
          }
          if (launchActivity) {
            launchActivity(getActivityIntent());
          }
          base.evaluate();
        } finally {
          if (instrumentation != null) {
            instrumentation.useDefaultInterceptingActivityFactory();
          }
  
          T hardActivityRef = activity.get();
          if (hardActivityRef != null) {
            finishActivity();
          }
          activityResult = null;
          ActivityLifecycleMonitorRegistry.getInstance().removeLifecycleCallback(lifecycleCallback);
        }
      }
    }
  
    @VisibleForTesting
    WeakReference<T> makeWeakReference(T activity) {
      return new WeakReference<T>(activity);
    }
  
    /**
     * Activity lifecycle callback which ensures to release a reference on the activity under test
     * after lifecycle changes. This is done to ensure that we don't leak the original Activity under
     * test and at the same time have a reference to the currently visible activity.
     *
     * <p>Note: this callback is run on the main thread!
     */
    private class LifecycleCallback implements ActivityLifecycleCallback {
      @Override
      public void onActivityLifecycleChanged(Activity activity, Stage stage) {
        if (activityClass.isInstance(activity)) {
          if (Stage.RESUMED == stage) {
            ActivityTestRule.this.activity = makeWeakReference(activityClass.cast(activity));
          } else if (Stage.PAUSED == stage) {
            // If there is an activity result we save it
            if (activity.isFinishing() && activityResult != null) {
              setActivityResultForActivity(activityClass.cast(activity));
            }
          }
        }
      }
    }
  }
  