Created
April 10, 2023 01:50
-
-
Save top-master/2127623cd837df9a9633714c44a56e16 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* 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 test.rules; | |
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 org.junit.rules.TestRule; | |
import org.junit.runner.Description; | |
import org.junit.runners.model.Statement; | |
import java.lang.ref.WeakReference; | |
import java.lang.reflect.Field; | |
/** | |
* 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 <a href="http://junit.org/javadoc/latest/org/junit/Test.html"><code>Test</code> | |
* </a> and before methods annotated with <a | |
* href="http://junit.sourceforge.net/javadoc/org/junit/Before.html"><code>Before</code></a>, and it | |
* will be terminated after the test is completed and methods annotated with <a | |
* href="http://junit.sourceforge.net/javadoc/org/junit/After.html"><code>After | |
* </code></a> 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. | |
* | |
* <p>Based on `androidx.test.rule.ActivityTestRule` class; | |
* <p>We un-deprecated it because this offers simpler testing approach, | |
* but consider to use {@link androidx.test.core.app.ActivityScenario} | |
* or {@link androidx.test.ext.junit.rules.ActivityScenarioRule} instead. | |
* They offer more complicated, but 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. Its 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> | |
* </p> | |
* | |
* @param <T> The Activity class under test | |
*/ | |
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 ActivityTestRule.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 <a href="http://junit.org/javadoc/latest/org/junit/Test.html"> | |
* <code>Test</code></a> method. It is launched before the first <a | |
* href="http://junit.sourceforge.net/javadoc/org/junit/Before.html"><code>Before</code></a> | |
* method, and terminated after the last <a | |
* href="http://junit.sourceforge.net/javadoc/org/junit/After.html"><code>After</code></a> 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#getInstrumentation()#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 <a | |
* href="http://junit.org/javadoc/latest/org/junit/Test.html"><code>Test</code></a> method. It | |
* will be launched before the first <a | |
* href="http://junit.sourceforge.net/javadoc/org/junit/Before.html"><code>Before</code></a> | |
* method, and terminated after the last <a | |
* href="http://junit.sourceforge.net/javadoc/org/junit/After.html"><code>After</code></a> | |
* 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 <a | |
* href="http://junit.org/javadoc/latest/org/junit/Test.html"><code>Test</code></a> method. It | |
* will be launched before the first <a | |
* href="http://junit.sourceforge.net/javadoc/org/junit/Before.html"><code>Before</code></a> | |
* method, and terminated after the last <a | |
* href="http://junit.sourceforge.net/javadoc/org/junit/After.html"><code>After</code></a> | |
* 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 <a | |
* href="http://junit.org/javadoc/latest/org/junit/Test.html"><code>Test</code></a> method. It | |
* will be launched before the first <a | |
* href="http://junit.sourceforge.net/javadoc/org/junit/Before.html"><code>Before</code></a> | |
* method, and terminated after the last <a | |
* href="http://junit.sourceforge.net/javadoc/org/junit/After.html"><code>After</code></a> | |
* 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 <a href="http://junit.sourceforge.net/javadoc/org/junit/Before.html"><code> | |
* Before</code></a>. | |
*/ | |
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 <a | |
* href="http://junit.sourceforge.net/javadoc/org/junit/Before.html"><code>Before</code></a>. | |
* | |
* <p>Prefer <a href="http://junit.sourceforge.net/javadoc/org/junit/Before.html"><code>Before | |
* </code></a> 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 <a href="http://junit.sourceforge.net/javadoc/org/junit/After.html"><code>After | |
* </code></a>. | |
* | |
* <p>Prefer <a href="http://junit.sourceforge.net/javadoc/org/junit/After.html"><code>Before | |
* </code></a> 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 single {@link Activity#onResume()} 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#onResume()} 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 ActivityTestRule.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> | |
* @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( | |
new Runnable() { | |
@Override | |
public void run() { | |
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( | |
new Runnable() { | |
@Override | |
public void run() { | |
checkState(hardActivityRef.isFinishing(), "Activity is not finishing!"); | |
setActivityResultForActivity(hardActivityRef); | |
} | |
}); | |
} catch (Throwable throwable) { | |
throw new IllegalStateException(throwable); | |
} | |
} | |
return activityResult; | |
} | |
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); | |
} | |
/** | |
* <a href="http://junit.org/apidocs/org/junit/runners/model/Statement.html"><code>Statement | |
* </code></a> 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)); | |
} | |
} | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment