java.lang.Object
↳Instrumentation
↳androidx.test.internal.runner.hidden.ExposedInstrumentationApi
↳androidx.test.runner.MonitoringInstrumentation
Subclasses:
AndroidJUnitRunner
Gradle dependencies
compile group: 'androidx.test', name: 'orchestrator', version: '1.5.0'
- groupId: androidx.test
- artifactId: orchestrator
- version: 1.5.0
Artifact androidx.test:orchestrator:1.5.0 it located at Google repository (https://maven.google.com/)
Androidx artifact mapping:
androidx.test:orchestrator com.android.support.test:orchestrator
Androidx class mapping:
androidx.test.runner.MonitoringInstrumentation android.support.test.runner.MonitoringInstrumentation
Overview
An instrumentation that enables several advanced features and makes some hard guarantees about
the state of the application under instrumentation.
A short list of these capabilities:
- Forces Application.onCreate() to happen before Instrumentation.onStart() runs (ensuring
your code always runs in a sane state).
- Logs application death due to exceptions.
- Allows tracking of activity lifecycle states.
- Registers instrumentation arguments in an easy to access place.
- Ensures your activities are creating themselves in reasonable amounts of time.
- Provides facilities to dump current app threads to test outputs.
- Ensures all activities finish before instrumentation exits.
This Instrumentation is *NOT* a test instrumentation (some of its subclasses are). It makes no
assumptions about what the subclass wants to do.
Summary
Methods |
---|
public void | callActivityOnCreate(Activity activity, Bundle bundle)
|
public void | callActivityOnDestroy(Activity activity)
|
public void | callActivityOnPause(Activity activity)
|
public void | callActivityOnRestart(Activity activity)
|
public void | callActivityOnResume(Activity activity)
|
public void | callActivityOnStart(Activity activity)
|
public void | callActivityOnStop(Activity activity)
|
public void | callApplicationOnCreate(Application app)
|
protected void | dumpThreadStateToOutputs(java.lang.String outputFileName)
|
public void | execStartActivities(Context who, IBinder contextThread, IBinder token, Activity target, Intent intents[], Bundle options)
|
public ActivityResult | execStartActivity(Context who, IBinder contextThread, IBinder token, Activity target, Intent intent, int requestCode)
|
public ActivityResult | execStartActivity(Context who, IBinder contextThread, IBinder token, Activity target, Intent intent, int requestCode, Bundle options)
|
public ActivityResult | execStartActivity(Context who, IBinder contextThread, IBinder token, Activity target, Intent intent, int requestCode, Bundle options, UserHandle user)
This API was added in Android API 17 (JELLY_BEAN_MR1) |
public ActivityResult | execStartActivity(Context who, IBinder contextThread, IBinder token, Fragment target, Intent intent, int requestCode, Bundle options)
|
public ActivityResult | execStartActivity(Context who, IBinder contextThread, IBinder token, java.lang.String target, Intent intent, int requestCode, Bundle options)
This API was added in Android API 23 (M) |
public void | finish(int resultCode, Bundle results)
Ensures all activities launched in this instrumentation are finished before the instrumentation
exits. |
protected java.lang.String | getThreadState()
|
protected void | installMultidex()
|
protected void | installOldMultiDex(java.lang.Class<java.lang.Object> multidexClass)
Perform application MultiDex installation only when instrumentation installation is not
available. |
public void | interceptActivityUsing(InterceptingActivityFactory interceptingActivityFactory)
Use the given InterceptingActivityFactory to create Activity instance in MonitoringInstrumentation.newActivity(ClassLoader, String, Intent). |
protected final boolean | isPrimaryInstrProcess()
Checks whether this instance of instrumentation should be considered as a primary
instrumentation process. |
protected boolean | isPrimaryInstrProcess(java.lang.String argsProcessName)
Checks whether this instance of instrumentation should be considered as a primary
instrumentation process. |
public Activity | newActivity(java.lang.Class<java.lang.Object> clazz, Context context, IBinder token, Application application, Intent intent, ActivityInfo info, java.lang.CharSequence title, Activity parent, java.lang.String id, java.lang.Object lastNonConfigurationInstance)
|
public Activity | newActivity(java.lang.ClassLoader cl, java.lang.String className, Intent intent)
|
public Application | newApplication(java.lang.ClassLoader cl, java.lang.String className, Context context)
Does initialization before creating the application. |
public void | onCreate(Bundle arguments)
Sets up lifecycle monitoring, and argument registry. |
public void | onDestroy()
|
public boolean | onException(java.lang.Object obj, java.lang.Throwable e)
|
public void | onStart()
This implementation of onStart() will guarantee that the Application's onCreate method has
completed when it returns. |
protected void | restoreUncaughtExceptionHandler()
|
public void | runOnMainSync(java.lang.Runnable runnable)
Posts a runnable to the main thread and blocks the caller's thread until the runnable is
executed. |
protected final void | setJsBridgeClassName(java.lang.String className)
|
protected boolean | shouldWaitForActivitiesToComplete()
|
protected void | specifyDexMakerCacheProperty()
|
public Activity | startActivitySync(Intent intent)
|
protected java.lang.Throwable | unwrapException(java.lang.Throwable t)
Unwraps an exception from certain wrapper types, e.g. |
public void | useDefaultInterceptingActivityFactory()
Use default mechanism of creating activity instance in MonitoringInstrumentation.newActivity(ClassLoader, String, Intent) |
protected void | waitForActivitiesToComplete()
Ensures we've onStopped() all activities which were onStarted(). |
from java.lang.Object | clone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait |
Constructors
public
MonitoringInstrumentation()
Methods
public Application
newApplication(java.lang.ClassLoader cl, java.lang.String className, Context context)
Does initialization before creating the application.
Note, MonitoringInstrumentation.newApplication(ClassLoader, String, Context) is called before MonitoringInstrumentation.onCreate(Bundle) on API > 15. For API <= 15, MonitoringInstrumentation.onCreate(Bundle) is called first.
public void
onCreate(Bundle arguments)
Sets up lifecycle monitoring, and argument registry.
Subclasses must call up to onCreate(). This onCreate method does not call start() it is the
subclasses responsibility to call start if it desires.
protected void
installMultidex()
protected void
installOldMultiDex(java.lang.Class<java.lang.Object> multidexClass)
Perform application MultiDex installation only when instrumentation installation is not
available. Called when MultiDex class is available but MultiDex.installInstrumentation is not.
protected void
specifyDexMakerCacheProperty()
protected final void
setJsBridgeClassName(java.lang.String className)
protected void
restoreUncaughtExceptionHandler()
This implementation of onStart() will guarantee that the Application's onCreate method has
completed when it returns.
Subclasses should call super.onStart() before executing any code that touches the
application and it's state.
public void
finish(int resultCode, Bundle results)
Ensures all activities launched in this instrumentation are finished before the instrumentation
exits.
Subclasses who override this method should do their finish processing and then call
super.finish to invoke this logic. Not waiting for all activities to finish() before exiting
can cause device wide instability.
protected boolean
shouldWaitForActivitiesToComplete()
protected void
waitForActivitiesToComplete()
Ensures we've onStopped() all activities which were onStarted().
According to Activity's contract, the process is not killable between onStart and onStop.
Breaking this contract (which finish() will if you let it) can cause bad behaviour (including a
full restart of system_server).
We give the app 2 seconds to stop all its activities, then we proceed.
This should never be run on the main thread.
public void
callApplicationOnCreate(Application app)
public void
runOnMainSync(java.lang.Runnable runnable)
Posts a runnable to the main thread and blocks the caller's thread until the runnable is
executed. When a Throwable is thrown in the runnable, the exception is propagated back to the
caller's thread. If it is an unchecked throwable, it will be rethrown as is. If it is a checked
exception, it will be rethrown as a java.lang.RuntimeException
.
Parameters:
runnable: a runnable to be executed on the main thread
public Activity
startActivitySync(Intent intent)
public ActivityResult
execStartActivity(Context who, IBinder contextThread, IBinder token, Activity target, Intent intent, int requestCode)
public ActivityResult
execStartActivity(Context who, IBinder contextThread, IBinder token, Activity target, Intent intent, int requestCode, Bundle options)
public ActivityResult
execStartActivity(Context who, IBinder contextThread, IBinder token, java.lang.String target, Intent intent, int requestCode, Bundle options)
This API was added in Android API 23 (M)
public ActivityResult
execStartActivity(Context who, IBinder contextThread, IBinder token, Activity target, Intent intent, int requestCode, Bundle options, UserHandle user)
This API was added in Android API 17 (JELLY_BEAN_MR1)
public void
execStartActivities(Context who, IBinder contextThread, IBinder token, Activity target, Intent intents[], Bundle options)
public ActivityResult
execStartActivity(Context who, IBinder contextThread, IBinder token, Fragment target, Intent intent, int requestCode, Bundle options)
public boolean
onException(java.lang.Object obj, java.lang.Throwable e)
protected void
dumpThreadStateToOutputs(java.lang.String outputFileName)
protected java.lang.String
getThreadState()
public void
callActivityOnDestroy(Activity activity)
public void
callActivityOnRestart(Activity activity)
public void
callActivityOnCreate(Activity activity, Bundle bundle)
public void
callActivityOnStart(Activity activity)
public void
callActivityOnStop(Activity activity)
public void
callActivityOnResume(Activity activity)
public void
callActivityOnPause(Activity activity)
public Activity
newActivity(java.lang.Class<java.lang.Object> clazz, Context context, IBinder token, Application application, Intent intent, ActivityInfo info, java.lang.CharSequence title, Activity parent, java.lang.String id, java.lang.Object lastNonConfigurationInstance)
public Activity
newActivity(java.lang.ClassLoader cl, java.lang.String className, Intent intent)
Use the given InterceptingActivityFactory to create Activity instance in MonitoringInstrumentation.newActivity(ClassLoader, String, Intent). This can be used to override default behavior of
activity in tests e.g. mocking startService() method in Activity under test, to avoid starting
the real service and instead verifying that a particular service was started.
Parameters:
interceptingActivityFactory: InterceptingActivityFactory to be used for creating activity
instance in MonitoringInstrumentation.newActivity(ClassLoader, String, Intent)
public void
useDefaultInterceptingActivityFactory()
Use default mechanism of creating activity instance in MonitoringInstrumentation.newActivity(ClassLoader, String, Intent)
protected boolean
isPrimaryInstrProcess(java.lang.String argsProcessName)
Deprecated: use isPrimaryInstrProcess()
Checks whether this instance of instrumentation should be considered as a primary
instrumentation process.
Prior to API 26, instrumentation could only run in a single process and that would be the
primary process. Post API 26, the primary process is the first process listed in
android:targetProcesses or the default process of the targetPackage.
Parameters:
argsProcessName: unused.
Returns:
true if the given process is the primary instrumentation process
protected final boolean
isPrimaryInstrProcess()
Checks whether this instance of instrumentation should be considered as a primary
instrumentation process.
Prior to API 26, instrumentation could only run in a single process and that would be the
primary process. Post API 26, the primary process is the first process listed in
android:targetProcesses or the default process of the targetPackage.
Returns:
true if the given process is the primary instrumentation process
protected java.lang.Throwable
unwrapException(java.lang.Throwable t)
Unwraps an exception from certain wrapper types, e.g. RuntimeException.
We consider two exceptions the same if they are caused by the same root cause. For example,
when an exception occurs on the main thread, the #onException method is first called to handle
it, and the exception is wrapped with RuntimeException and re-thrown, which is then caught by
the uncaught exception handler. In this case, the exception should only be handled once.
Returns:
the root cause of the given exception
Source
/*
* Copyright (C) 2015 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.runner;
import static androidx.test.internal.util.Checks.checkNotMainThread;
import android.app.Activity;
import android.app.Application;
import android.app.Fragment;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.MessageQueue.IdleHandler;
import android.os.UserHandle;
import android.util.Log;
import androidx.annotation.Nullable;
import androidx.test.internal.platform.app.ActivityLifecycleTimeout;
import androidx.test.internal.runner.InstrumentationConnection;
import androidx.test.internal.runner.hidden.ExposedInstrumentationApi;
import androidx.test.internal.runner.intent.IntentMonitorImpl;
import androidx.test.internal.runner.intercepting.DefaultInterceptingActivityFactory;
import androidx.test.internal.runner.lifecycle.ActivityLifecycleMonitorImpl;
import androidx.test.internal.runner.lifecycle.ApplicationLifecycleMonitorImpl;
import androidx.test.internal.util.Checks;
import androidx.test.internal.util.ProcSummary;
import androidx.test.platform.app.AppComponentFactoryRegistry;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.runner.intent.IntentMonitorRegistry;
import androidx.test.runner.intent.IntentStubberRegistry;
import androidx.test.runner.intercepting.InterceptingActivityFactory;
import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry;
import androidx.test.runner.lifecycle.ApplicationLifecycleMonitorRegistry;
import androidx.test.runner.lifecycle.ApplicationStage;
import androidx.test.runner.lifecycle.Stage;
import androidx.tracing.Trace;
import java.io.File;
import java.lang.Thread.UncaughtExceptionHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
/**
* An instrumentation that enables several advanced features and makes some hard guarantees about
* the state of the application under instrumentation.
*
* <p>A short list of these capabilities:
*
* <ul>
* <li>Forces Application.onCreate() to happen before Instrumentation.onStart() runs (ensuring
* your code always runs in a sane state).
* <li>Logs application death due to exceptions.
* <li>Allows tracking of activity lifecycle states.
* <li>Registers instrumentation arguments in an easy to access place.
* <li>Ensures your activities are creating themselves in reasonable amounts of time.
* <li>Provides facilities to dump current app threads to test outputs.
* <li>Ensures all activities finish before instrumentation exits.
* </ul>
*
* This Instrumentation is *NOT* a test instrumentation (some of its subclasses are). It makes no
* assumptions about what the subclass wants to do.
*/
public class MonitoringInstrumentation extends ExposedInstrumentationApi {
private static final String TAG = "MonitoringInstr";
private static final long MILLIS_TO_WAIT_FOR_ACTIVITY_TO_STOP = TimeUnit.SECONDS.toMillis(2);
private static final long MILLIS_TO_POLL_FOR_ACTIVITY_STOP =
MILLIS_TO_WAIT_FOR_ACTIVITY_TO_STOP / 40;
private ActivityLifecycleMonitorImpl lifecycleMonitor = new ActivityLifecycleMonitorImpl();
private ApplicationLifecycleMonitorImpl applicationMonitor =
new ApplicationLifecycleMonitorImpl();
private IntentMonitorImpl intentMonitor = new IntentMonitorImpl();
private ExecutorService executorService;
private Handler handlerForMainLooper;
private AtomicBoolean anActivityHasBeenLaunched = new AtomicBoolean(false);
private AtomicLong lastIdleTime = new AtomicLong(0);
private AtomicInteger startedActivityCounter = new AtomicInteger(0);
private String jsBridgeClassName;
private AtomicBoolean isJsBridgeLoaded = new AtomicBoolean(false);
// read from many threads / written by many threads.
// if null will be calculated in the same way by any thread.
private volatile Boolean isOriginalInstr = null;
private ThreadLocal<Boolean> isDexmakerClassLoaderInitialized = new ThreadLocal<>();
private IdleHandler idleHandler =
new IdleHandler() {
@Override
public boolean queueIdle() {
lastIdleTime.set(System.currentTimeMillis());
return true;
}
};
private volatile boolean finished = false;
private volatile InterceptingActivityFactory interceptingActivityFactory;
private UncaughtExceptionHandler oldDefaultExceptionHandler;
/**
* Does initialization before creating the application.
*
* <p>Note, {@link #newApplication(ClassLoader, String, Context)} is called before {@link
* #onCreate(Bundle)} on API > 15. For API <= 15, {@link #onCreate(Bundle)} is called first.
*/
@Override
public Application newApplication(ClassLoader cl, String className, Context context)
throws InstantiationException, IllegalAccessException, ClassNotFoundException {
installMultidexAndExceptionHandler();
Application application = AppComponentFactoryRegistry.instantiateApplication(cl, className);
if (application != null) {
return application;
}
return super.newApplication(cl, className, context);
}
/**
* Sets up lifecycle monitoring, and argument registry.
*
* <p>Subclasses must call up to onCreate(). This onCreate method does not call start() it is the
* subclasses responsibility to call start if it desires.
*/
@Override
public void onCreate(Bundle arguments) {
InstrumentationRegistry.registerInstance(this, arguments);
androidx.test.InstrumentationRegistry.registerInstance(this, arguments);
ActivityLifecycleMonitorRegistry.registerInstance(lifecycleMonitor);
ApplicationLifecycleMonitorRegistry.registerInstance(applicationMonitor);
IntentMonitorRegistry.registerInstance(intentMonitor);
handlerForMainLooper = new Handler(Looper.getMainLooper());
final int corePoolSize = 0;
final long keepAliveTime = 0L;
executorService =
new ThreadPoolExecutor(
corePoolSize,
Integer.MAX_VALUE,
keepAliveTime,
TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
new ThreadFactory() {
@Override
public Thread newThread(Runnable runnable) {
Thread thread = Executors.defaultThreadFactory().newThread(runnable);
thread.setName(MonitoringInstrumentation.class.getSimpleName());
return thread;
}
});
Looper.myQueue().addIdleHandler(idleHandler);
super.onCreate(arguments);
specifyDexMakerCacheProperty();
setupDexmakerClassloader();
useDefaultInterceptingActivityFactory();
}
protected void installMultidex() {
// Typically multidex is installed by inserting call at Application#attachBaseContext
// However instrumentation#onCreate is called before Application#attachBaseContext. Thus
// need to install it here, if its on classpath.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
try {
Class<?> multidex = getMultiDexClass();
try {
Method installInstrumentation =
multidex.getDeclaredMethod("installInstrumentation", Context.class, Context.class);
installInstrumentation.invoke(null, getContext(), getTargetContext());
} catch (NoSuchMethodException nsme) {
Log.w(
TAG,
"Could not find MultiDex.installInstrumentation. Calling MultiDex.install instead."
+ " Is an old version of the multidex library being used? If test app is using"
+ " multidex, classes might not be found");
installOldMultiDex(multidex);
}
} catch (ClassNotFoundException ignored) {
Log.i(TAG, "No multidex.");
} catch (NoSuchMethodException nsme) {
Log.i(TAG, "No multidex.", nsme);
} catch (InvocationTargetException ite) {
throw new RuntimeException("multidex is available at runtime, but calling it failed.", ite);
} catch (IllegalAccessException iae) {
throw new RuntimeException("multidex is available at runtime, but calling it failed.", iae);
}
}
}
private static Class<?> getMultiDexClass() throws ClassNotFoundException {
try {
return Class.forName("androidx.multidex.MultiDex");
} catch (ClassNotFoundException e) {
// check for support multidex
return Class.forName("android.support.multidex.MultiDex");
}
}
/**
* Perform application MultiDex installation only when instrumentation installation is not
* available. Called when MultiDex class is available but MultiDex.installInstrumentation is not.
*/
protected void installOldMultiDex(Class<?> multidexClass)
throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Method install = multidexClass.getDeclaredMethod("install", Context.class);
install.invoke(null, getTargetContext());
}
protected void specifyDexMakerCacheProperty() {
// DexMaker uses heuristics to figure out where to store its temporary dex files
// these heuristics may break (eg - they no longer work on JB MR2). So we create
// our own cache dir to be used if the app doesnt specify a cache dir, rather then
// relying on heuristics.
//
File dexCache = getTargetContext().getDir("dxmaker_cache", Context.MODE_PRIVATE);
System.getProperties().put("dexmaker.dexcache", dexCache.getAbsolutePath());
}
protected final void setJsBridgeClassName(final String className) {
if (null == className) {
throw new NullPointerException("JsBridge class name cannot be null!");
}
if (isJsBridgeLoaded.get()) {
throw new IllegalStateException("JsBridge is already loaded!");
}
jsBridgeClassName = className;
}
private void setupDexmakerClassloader() {
if (Boolean.TRUE.equals(isDexmakerClassLoaderInitialized.get())) {
// We've already setup dexmaker for this ContextClassLoader, so let's not mess with
// the user.
return;
}
ClassLoader originalClassLoader = Thread.currentThread().getContextClassLoader();
// must set the context classloader for apps that use a shared uid, see
// frameworks/base/core/java/android/app/LoadedApk.java
ClassLoader newClassLoader = getTargetContext().getClassLoader();
if (originalClassLoader != newClassLoader) {
Log.i(
TAG,
String.format(
"Setting context classloader to '%s', Original: '%s'",
newClassLoader, originalClassLoader));
Thread.currentThread().setContextClassLoader(newClassLoader);
}
isDexmakerClassLoaderInitialized.set(Boolean.TRUE);
}
private void installMultidexAndExceptionHandler() {
// Multidex must be installed early otherwise we could call into code that has
// landed in a different dex split.
installMultidex();
// App could crash even before #onCreate(Bundle) method is called, e.g. during installing
// content providers.
// Registers a custom uncaught exception handler to shuttle back the exception to the
// instrumentation result.
registerUncaughtExceptionHandler();
}
private void registerUncaughtExceptionHandler() {
oldDefaultExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
Thread.setDefaultUncaughtExceptionHandler(
new UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
Log.d(
TAG,
String.format(
"Handling an uncaught exception thrown on the thread %s.", t.getName()),
e);
onException(t, e);
if (null != oldDefaultExceptionHandler) {
Log.w(
TAG,
String.format(
"Invoking default uncaught exception handler %s (a %s)",
oldDefaultExceptionHandler, oldDefaultExceptionHandler.getClass()));
oldDefaultExceptionHandler.uncaughtException(t, e);
}
if (!"robolectric".equals(Build.FINGERPRINT)
&& Looper.getMainLooper().getThread().equals(t)) {
// Running within the Android OS and the handler didn't kill the main thread
// Now we're in a state where the main looper is stopped and the Android OS
// can no longer commmunicate with the app - by crashing we ensure the
// am instrument command exits cleanly.
Log.e(TAG, "The main thread has died and the handlers didn't care, exiting");
System.exit(-10);
}
}
});
}
protected void restoreUncaughtExceptionHandler() {
Thread.setDefaultUncaughtExceptionHandler(oldDefaultExceptionHandler);
}
/**
* This implementation of onStart() will guarantee that the Application's onCreate method has
* completed when it returns.
*
* <p>Subclasses should call super.onStart() before executing any code that touches the
* application and it's state.
*/
@Override
public void onStart() {
super.onStart();
if (jsBridgeClassName != null) {
tryLoadingJsBridge(jsBridgeClassName);
}
// Due to the way Android initializes instrumentation - all instrumentations have the
// possibility of seeing the Application and its classes in an inconsistent state.
// Specifically ActivityThread creates Instrumentation first, initializes it, and calls
// instrumentation.onCreate(). After it does that, it calls
// instrumentation.callApplicationOnCreate() which ends up calling the application's
// onCreateMethod.
//
// So, Android's InstrumentationTestRunner's onCreate method() spawns a separate thread to
// execute tests. This causes tests to start accessing the application and its classes while
// the ActivityThread is calling callApplicationOnCreate() in its own thread.
//
// This makes it possible for tests to see the application in a state that is normally never
// visible: pre-application.onCreate() and during application.onCreate()).
//
// *phew* that sucks! Here we waitForOnIdleSync() to ensure onCreate has completed before we
// start executing tests.
waitForIdleSync();
// If the user has not yet set up a ContextClassLoader, they may need one set up for them
// now. They cannot see the one we set up in onCreate() because that took place on a
// different thread.
setupDexmakerClassloader();
InstrumentationConnection.getInstance().init(this, new ActivityFinisher());
}
/**
* Ensures all activities launched in this instrumentation are finished before the instrumentation
* exits.
*
* <p>Subclasses who override this method should do their finish processing and then call
* super.finish to invoke this logic. Not waiting for all activities to finish() before exiting
* can cause device wide instability.
*/
@Override
public void finish(int resultCode, Bundle results) {
if (finished) {
Log.w(TAG, "finish called 2x!");
return;
} else {
finished = true;
}
Trace.beginSection("MonitoringInstrumentation#finish");
if (shouldWaitForActivitiesToComplete()) {
handlerForMainLooper.post(new ActivityFinisher());
waitForActivitiesToComplete();
}
ActivityLifecycleMonitorRegistry.registerInstance(null);
restoreUncaughtExceptionHandler();
Trace.endSection();
// super.finish kills the current process, so this needs to be called last
super.finish(resultCode, results);
}
protected boolean shouldWaitForActivitiesToComplete() {
// TODO(b/72831103): default this argument to false in a future release
return Boolean.parseBoolean(
InstrumentationRegistry.getArguments().getString("waitForActivitiesToComplete", "true"));
}
/**
* Ensures we've onStopped() all activities which were onStarted().
*
* <p>According to Activity's contract, the process is not killable between onStart and onStop.
* Breaking this contract (which finish() will if you let it) can cause bad behaviour (including a
* full restart of system_server).
*
* <p>We give the app 2 seconds to stop all its activities, then we proceed.
*
* <p>This should never be run on the main thread.
*/
protected void waitForActivitiesToComplete() {
if (Looper.getMainLooper() == Looper.myLooper()) {
throw new IllegalStateException("Cannot be called from main thread!");
}
long endTime = System.currentTimeMillis() + MILLIS_TO_WAIT_FOR_ACTIVITY_TO_STOP;
int currentActivityCount = startedActivityCounter.get();
while (currentActivityCount > 0 && System.currentTimeMillis() < endTime) {
try {
Log.i(TAG, "Unstopped activity count: " + currentActivityCount);
Thread.sleep(MILLIS_TO_POLL_FOR_ACTIVITY_STOP);
currentActivityCount = startedActivityCounter.get();
} catch (InterruptedException ie) {
Log.i(TAG, "Abandoning activity wait due to interruption.", ie);
break;
}
}
if (currentActivityCount > 0) {
dumpThreadStateToOutputs("ThreadState-unstopped.txt");
Log.w(
TAG,
String.format(
"Still %s activities active after waiting %s ms.",
currentActivityCount, MILLIS_TO_WAIT_FOR_ACTIVITY_TO_STOP));
}
}
@Override
public void onDestroy() {
Log.i(TAG, "Instrumentation Finished!");
Looper.myQueue().removeIdleHandler(idleHandler);
InstrumentationConnection.getInstance().terminate();
super.onDestroy();
}
@Override
public void callApplicationOnCreate(Application app) {
applicationMonitor.signalLifecycleChange(app, ApplicationStage.PRE_ON_CREATE);
super.callApplicationOnCreate(app);
applicationMonitor.signalLifecycleChange(app, ApplicationStage.CREATED);
}
/**
* Posts a runnable to the main thread and blocks the caller's thread until the runnable is
* executed. When a Throwable is thrown in the runnable, the exception is propagated back to the
* caller's thread. If it is an unchecked throwable, it will be rethrown as is. If it is a checked
* exception, it will be rethrown as a {@link RuntimeException}.
*
* @param runnable a runnable to be executed on the main thread
*/
@Override
public void runOnMainSync(Runnable runnable) {
FutureTask<Void> wrapped = new FutureTask<>(runnable, null);
super.runOnMainSync(wrapped);
try {
wrapped.get();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (ExecutionException e) {
Throwable cause = e.getCause();
if (cause instanceof RuntimeException) {
throw (RuntimeException) cause;
} else if (cause instanceof Error) {
throw (Error) cause;
}
throw new RuntimeException(cause);
}
}
@Override
public Activity startActivitySync(final Intent intent) {
checkNotMainThread();
long lastIdleTimeBeforeLaunch = lastIdleTime.get();
if (anActivityHasBeenLaunched.compareAndSet(false, true)) {
// All activities launched from InstrumentationTestCase.launchActivityWithIntent get
// started with FLAG_ACTIVITY_NEW_TASK. This includes calls to
// ActivityInstrumentationTestcase2.getActivity().
//
// This gives us a pristine environment - MOST OF THE TIME.
//
// However IF we've run a test method previously and that has launched an activity
// outside of our process our old task is still lingering around. By launching a new
// activity android will place our activity at the bottom of the stack and bring the
// previous external activity to the front of the screen.
//
// To wipe out the old task and execute within a pristine environment for each test
// we tell android to CLEAR_TOP the very first activity we see, no matter what.
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
}
Future<Activity> startedActivity =
executorService.submit(
new Callable<Activity>() {
@Override
public Activity call() {
return MonitoringInstrumentation.super.startActivitySync(intent);
}
});
try {
return startedActivity.get(ActivityLifecycleTimeout.getMillis(), TimeUnit.MILLISECONDS);
} catch (TimeoutException te) {
dumpThreadStateToOutputs("ThreadState-startActivityTimeout.txt");
startedActivity.cancel(true);
throw new RuntimeException(
String.format(
"Could not launch intent %s within %s milliseconds."
+ " Perhaps the main thread has not gone idle within a reasonable amount of "
+ "time? There could be an animation or something constantly repainting the "
+ "screen. Or the activity is doing network calls on creation? See the "
+ "threaddump logs. For your reference the last time the event queue was idle "
+ "before your activity launch request was %s and now the last time the queue "
+ "went idle was: %s. If these numbers are the same your activity might be "
+ "hogging the event queue.",
intent,
ActivityLifecycleTimeout.getMillis(),
lastIdleTimeBeforeLaunch,
lastIdleTime.get()));
} catch (ExecutionException ee) {
throw new RuntimeException("Could not launch activity", ee.getCause());
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new RuntimeException("interrupted", ie);
}
}
/** {@inheritDoc} */
@Override
public ActivityResult execStartActivity(
Context who,
IBinder contextThread,
IBinder token,
Activity target,
Intent intent,
int requestCode) {
intentMonitor.signalIntent(intent);
ActivityResult ar = stubResultFor(intent);
if (ar != null) {
Log.i(TAG, String.format("Stubbing intent %s", intent));
return ar;
}
return super.execStartActivity(who, contextThread, token, target, intent, requestCode);
}
/** {@inheritDoc} */
@Override
public ActivityResult execStartActivity(
Context who,
IBinder contextThread,
IBinder token,
Activity target,
Intent intent,
int requestCode,
Bundle options) {
intentMonitor.signalIntent(intent);
ActivityResult ar = stubResultFor(intent);
if (ar != null) {
Log.i(TAG, String.format("Stubbing intent %s", intent));
return ar;
}
return super.execStartActivity(who, contextThread, token, target, intent, requestCode, options);
}
/** This API was added in Android API 23 (M) */
@Override
public ActivityResult execStartActivity(
Context who,
IBinder contextThread,
IBinder token,
String target,
Intent intent,
int requestCode,
Bundle options) {
intentMonitor.signalIntent(intent);
ActivityResult ar = stubResultFor(intent);
if (ar != null) {
Log.i(TAG, String.format("Stubbing intent %s", intent));
return ar;
}
return super.execStartActivity(who, contextThread, token, target, intent, requestCode, options);
}
/** This API was added in Android API 17 (JELLY_BEAN_MR1) */
@Override
public ActivityResult execStartActivity(
Context who,
IBinder contextThread,
IBinder token,
Activity target,
Intent intent,
int requestCode,
Bundle options,
UserHandle user) {
return super.execStartActivity(
who, contextThread, token, target, intent, requestCode, options, user);
}
/** {@inheritDoc} */
@Override
public void execStartActivities(
Context who,
IBinder contextThread,
IBinder token,
Activity target,
Intent[] intents,
Bundle options) {
// This method is used to create a synthetic back stack for the
// launched activity. The intent at the end of the array is the top most, user visible
// activity, and the intents beneath it are launched when the user presses back.
Log.d(TAG, "execStartActivities(context, ibinder, ibinder, activity, intent[], bundle)");
// For requestCode < 0, the caller doesn't expect any result and
// in this case we are not expecting any result so selecting
// a value < 0.
int requestCode = -1;
for (Intent intent : intents) {
execStartActivity(who, contextThread, token, target, intent, requestCode, options);
}
}
/** {@inheritDoc} */
@Override
public ActivityResult execStartActivity(
Context who,
IBinder contextThread,
IBinder token,
Fragment target,
Intent intent,
int requestCode,
Bundle options) {
Log.d(TAG, "execStartActivity(context, IBinder, IBinder, Fragment, Intent, int, Bundle)");
intentMonitor.signalIntent(intent);
ActivityResult ar = stubResultFor(intent);
if (ar != null) {
Log.i(TAG, String.format("Stubbing intent %s", intent));
return ar;
}
return super.execStartActivity(who, contextThread, token, target, intent, requestCode, options);
}
private static class StubResultCallable implements Callable<ActivityResult> {
private final Intent intent;
StubResultCallable(Intent intent) {
this.intent = intent;
}
@Override
public ActivityResult call() {
return IntentStubberRegistry.getInstance().getActivityResultForIntent(intent);
}
}
private ActivityResult stubResultFor(Intent intent) {
if (IntentStubberRegistry.isLoaded()) {
// Activities can be launched from the instrumentation thread, so if that's the case,
// get on main thread to retrieve the result.
if (Looper.myLooper() != Looper.getMainLooper()) {
FutureTask<ActivityResult> task =
new FutureTask<ActivityResult>(new StubResultCallable(intent));
runOnMainSync(task);
try {
return task.get();
} catch (ExecutionException e) {
String msg = String.format("Could not retrieve stub result for intent %s", intent);
// try to preserve original exception
if (e.getCause() instanceof RuntimeException) {
Log.w(TAG, msg, e);
throw (RuntimeException) e.getCause();
} else if (e.getCause() != null) {
throw new RuntimeException(msg, e.getCause());
} else {
throw new RuntimeException(msg, e);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
}
} else {
return IntentStubberRegistry.getInstance().getActivityResultForIntent(intent);
}
}
return null;
}
@Override
public boolean onException(Object obj, Throwable e) {
String error =
String.format(
"Exception encountered by: %s. Dumping thread state to "
+ "outputs and pining for the fjords.",
obj);
Log.e(TAG, error, e);
dumpThreadStateToOutputs("ThreadState-onException.txt");
Log.e(TAG, "Dying now...");
return super.onException(obj, e);
}
protected void dumpThreadStateToOutputs(String outputFileName) {
String threadState = getThreadState();
Log.e("THREAD_STATE", threadState);
}
protected String getThreadState() {
Set<Map.Entry<Thread, StackTraceElement[]>> threads = Thread.getAllStackTraces().entrySet();
StringBuilder threadState = new StringBuilder();
for (Map.Entry<Thread, StackTraceElement[]> threadAndStack : threads) {
StringBuilder threadMessage = new StringBuilder(" ").append(threadAndStack.getKey());
threadMessage.append("\n");
for (StackTraceElement ste : threadAndStack.getValue()) {
threadMessage.append(" ");
threadMessage.append(ste.toString());
threadMessage.append("\n");
}
threadMessage.append("\n");
threadState.append(threadMessage.toString());
}
return threadState.toString();
}
@Override
public void callActivityOnDestroy(Activity activity) {
super.callActivityOnDestroy(activity);
lifecycleMonitor.signalLifecycleChange(Stage.DESTROYED, activity);
}
@Override
public void callActivityOnRestart(Activity activity) {
super.callActivityOnRestart(activity);
lifecycleMonitor.signalLifecycleChange(Stage.RESTARTED, activity);
}
@Override
public void callActivityOnCreate(Activity activity, Bundle bundle) {
lifecycleMonitor.signalLifecycleChange(Stage.PRE_ON_CREATE, activity);
super.callActivityOnCreate(activity, bundle);
lifecycleMonitor.signalLifecycleChange(Stage.CREATED, activity);
}
// NOTE: we need to keep a count of activities between the start
// and stop lifecycle internal to our instrumentation. Exiting the test
// process with activities in this state can cause crashes/flakiness
// that would impact a subsequent test run.
@Override
public void callActivityOnStart(Activity activity) {
startedActivityCounter.incrementAndGet();
try {
super.callActivityOnStart(activity);
lifecycleMonitor.signalLifecycleChange(Stage.STARTED, activity);
} catch (RuntimeException re) {
startedActivityCounter.decrementAndGet();
throw re;
}
}
@Override
public void callActivityOnStop(Activity activity) {
try {
super.callActivityOnStop(activity);
lifecycleMonitor.signalLifecycleChange(Stage.STOPPED, activity);
} finally {
startedActivityCounter.decrementAndGet();
}
}
@Override
public void callActivityOnResume(Activity activity) {
super.callActivityOnResume(activity);
lifecycleMonitor.signalLifecycleChange(Stage.RESUMED, activity);
}
@Override
public void callActivityOnPause(Activity activity) {
super.callActivityOnPause(activity);
lifecycleMonitor.signalLifecycleChange(Stage.PAUSED, activity);
}
// ActivityUnitTestCase defaults to building the ComponentName via
// Activity.getClass().getPackage().getName(). This will cause a problem if the Java Package of
// the Activity is not the Android Package of the application, specifically
// Activity.getPackageName() will return an incorrect value.
// @see b/14561718
@Override
public Activity newActivity(
Class<?> clazz,
Context context,
IBinder token,
Application application,
Intent intent,
ActivityInfo info,
CharSequence title,
Activity parent,
String id,
Object lastNonConfigurationInstance)
throws InstantiationException, IllegalAccessException {
String activityClassPackageName = clazz.getPackage().getName();
String contextPackageName = context.getPackageName();
ComponentName intentComponentName = intent.getComponent();
if (!contextPackageName.equals(intentComponentName.getPackageName())) {
if (activityClassPackageName.equals(intentComponentName.getPackageName())) {
intent.setComponent(
new ComponentName(contextPackageName, intentComponentName.getClassName()));
}
}
return super.newActivity(
clazz,
context,
token,
application,
intent,
info,
title,
parent,
id,
lastNonConfigurationInstance);
}
@Override
public Activity newActivity(ClassLoader cl, String className, Intent intent)
throws InstantiationException, IllegalAccessException, ClassNotFoundException {
if (interceptingActivityFactory.shouldIntercept(cl, className, intent)) {
return interceptingActivityFactory.create(cl, className, intent);
}
Activity activity = AppComponentFactoryRegistry.instantiateActivity(cl, className, intent);
if (activity != null) {
return activity;
}
return super.newActivity(cl, className, intent);
}
/**
* Use the given InterceptingActivityFactory to create Activity instance in {@link
* #newActivity(ClassLoader, String, Intent)}. This can be used to override default behavior of
* activity in tests e.g. mocking startService() method in Activity under test, to avoid starting
* the real service and instead verifying that a particular service was started.
*
* @param interceptingActivityFactory InterceptingActivityFactory to be used for creating activity
* instance in {@link #newActivity(ClassLoader, String, Intent)}
*/
public void interceptActivityUsing(InterceptingActivityFactory interceptingActivityFactory) {
Checks.checkNotNull(interceptingActivityFactory);
this.interceptingActivityFactory = interceptingActivityFactory;
}
/**
* Use default mechanism of creating activity instance in {@link #newActivity(ClassLoader, String,
* Intent)}
*/
public void useDefaultInterceptingActivityFactory() {
interceptingActivityFactory = new DefaultInterceptingActivityFactory();
}
/**
* Loads the JS Bridge for Espresso Web. This method will be ran on the main thread!
*
* @param className the name of the JsBridge class
*/
private void tryLoadingJsBridge(final String className) {
if (null == className) {
throw new NullPointerException("JsBridge class name cannot be null!");
}
runOnMainSync(
new Runnable() {
@Override
public void run() {
try {
Class<?> jsBridge = Class.forName(className);
Method install = jsBridge.getDeclaredMethod("installBridge");
install.invoke(null);
isJsBridgeLoaded.set(true);
} catch (ClassNotFoundException ignored) {
// ignore
} catch (NoSuchMethodException ignored) {
// ignore
} catch (InvocationTargetException ite) {
throw new RuntimeException(
"JSbridge is available at runtime, but calling it failed.", ite);
} catch (IllegalAccessException iae) {
throw new RuntimeException(
"JSbridge is available at runtime, but calling it failed.", iae);
}
}
});
}
/**
* Loops through all the activities that have not yet finished and explicitly calls finish on
* them.
*/
public class ActivityFinisher implements Runnable {
@Override
public void run() {
List<Activity> activities = new ArrayList<>();
for (Stage s : EnumSet.range(Stage.CREATED, Stage.STOPPED)) {
activities.addAll(lifecycleMonitor.getActivitiesInStage(s));
}
for (Activity activity : activities) {
if (!activity.isFinishing()) {
try {
Log.i(TAG, "Finishing activity: " + activity);
activity.finish();
} catch (RuntimeException e) {
Log.e(TAG, "Failed to finish activity.", e);
}
}
}
}
}
/**
* Checks whether this instance of instrumentation should be considered as a primary
* instrumentation process.
*
* <p>Prior to API 26, instrumentation could only run in a single process and that would be the
* primary process. Post API 26, the primary process is the first process listed in
* android:targetProcesses or the default process of the targetPackage.
*
* @deprecated use isPrimaryInstrProcess()
* @param argsProcessName unused.
* @return {@code true} if the given process is the primary instrumentation process
*/
@Deprecated
protected boolean isPrimaryInstrProcess(@Nullable String argsProcessName) {
return isPrimaryInstrProcess();
}
/**
* Checks whether this instance of instrumentation should be considered as a primary
* instrumentation process.
*
* <p>Prior to API 26, instrumentation could only run in a single process and that would be the
* primary process. Post API 26, the primary process is the first process listed in
* android:targetProcesses or the default process of the targetPackage.
*
* @return {@code true} if the given process is the primary instrumentation process
*/
protected final boolean isPrimaryInstrProcess() {
return isOriginalInstrumentationProcess();
}
/** Determines if a given ProcSummary is hosting a particular android:process. */
private boolean isHostingProcess(String wantName, ProcSummary ps) {
int wantLen = wantName.length();
int cmdLen = ps.cmdline.length();
if (wantLen == cmdLen) {
// not truncated - just .equals()
return wantName.equals(ps.cmdline);
} else if (wantLen < cmdLen) {
// obviously not the same.
return false;
} else {
// ProcSummary.cmdline length is limited to the size of the original zygote cmdline
// (See Process.setARGV0 / app_main.cpp computeArgBlockSize)
//
// cmdline is truncated to this length if process name is longer.
// Interestingly ps.name (which is /proc/pid/comm / pthread_getname / PR_GET_NAME)
// is the last 15 chars (See com_android_internal_os_Zygote / SetThreadName /
// MAX_TASK_COMM_LEN in bionic.
//
// So if wantName.substring(0, ps.cmdline.length()).equals(ps.cmdline) &&
// wantName.substring(wantName.length() - ps.name.length()).equals(ps.name) then
// most likely - they are the same.
if (wantName.startsWith(ps.cmdline)) {
if (wantName.endsWith(ps.name)) {
Log.w(
TAG,
"Use smaller processNames in AndroidManifest.xml. Long names are truncated. "
+ "This process's cmdline is a prefix of the processName and suffix of "
+ "comm - assuming: "
+ ps
+ " is: "
+ wantName);
return true;
}
}
return false;
}
}
private boolean isOriginalInstrumentationProcess() {
Boolean isOriginal = isOriginalInstr;
if (isOriginal == null) {
isOriginal = isOriginalUncached();
isOriginalInstr = isOriginal;
}
return isOriginal;
}
private List<String> getTargetProcessValues() {
if (Build.VERSION.SDK_INT < 26) {
return Collections.emptyList();
}
String tpVal = null;
try {
tpVal =
getContext()
.getPackageManager()
.getInstrumentationInfo(getComponentName(), 0)
.targetProcesses;
if (tpVal == null) {
tpVal = "";
}
tpVal = tpVal.trim();
} catch (PackageManager.NameNotFoundException unpossible) {
Log.wtf(TAG, "Cannot locate ourselves: " + getComponentName(), unpossible);
throw new IllegalStateException("Cannot locate ourselves: " + getComponentName(), unpossible);
}
if (tpVal.length() == 0) {
return Collections.emptyList();
}
List<String> tps = new ArrayList<>();
for (String tp : tpVal.split(",", -1)) {
tp = tp.trim();
if (tp.length() > 0) {
tps.add(tp);
}
}
return tps;
}
private boolean isOriginalUncached() {
if (Build.VERSION.SDK_INT < 26) {
return true;
}
List<String> targetProcesses = getTargetProcessValues();
if (targetProcesses.isEmpty()) {
// if android:targetProcesses is unspecified, android will only launch instr in 1 process.
return true;
}
boolean isWildcard = "*".equals(targetProcesses.get(0));
if (targetProcesses.size() == 1 && !isWildcard) {
// if android:targetProcesses is specified and not wildcarded, the system will only launch
// instr in 1 process.
return true;
}
ProcSummary me = null;
try {
me = ProcSummary.summarize("self");
} catch (ProcSummary.SummaryException se) {
Log.w(TAG, "Could not list apps for this user, running in sandbox? Assuming primary", se);
return false;
}
if (isWildcard) {
// in wildcard mode treat the default app process as the primary process.
String appDefProcessName = getTargetContext().getApplicationInfo().processName;
if (appDefProcessName == null) {
appDefProcessName = getTargetContext().getPackageName();
}
return isHostingProcess(appDefProcessName, me);
} else {
// in targeted mode treat the 1st name listed in android:targetProcesses as primary.
return isHostingProcess(targetProcesses.get(0), me);
}
}
/**
* Unwraps an exception from certain wrapper types, e.g. RuntimeException.
*
* <p>We consider two exceptions the same if they are caused by the same root cause. For example,
* when an exception occurs on the main thread, the #onException method is first called to handle
* it, and the exception is wrapped with RuntimeException and re-thrown, which is then caught by
* the uncaught exception handler. In this case, the exception should only be handled once.
*
* @return the root cause of the given exception
*/
protected Throwable unwrapException(Throwable t) {
Throwable cause = t.getCause();
if (cause == null) {
return t;
}
Class<?> clazz = t.getClass();
if (clazz.equals(RuntimeException.class)) {
return unwrapException(cause);
}
return t;
}
}