package com.google.android.apps.common.testing.testrunner;
import com.google.android.apps.common.testing.intento.IntentSpy;
import android.app.Activity;
import android.app.Fragment;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.MessageQueue.IdleHandler;
import android.util.Log;
import java.io.File;
import java.io.Writer;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import java.util.Locale;
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.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.
*
* 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 activites are creating themselves in reasonable amounts of time.
* - Provides facilties 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.
*
*/
public class
GoogleInstrumentation
extends ExposedInstrumentationApi {
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 static final String LOG_TAG = "GoogleInstr";
private static final String DEFAULT_EMULATOR_IME = "com.android.inputmethod.latin/.LatinIME";
private static final String DEFAULT_PHONE_IME =
"com.google.android.inputmethod.latin/com.android.inputmethod.latin.LatinIME";
private static final Locale DEFAULT_LOCALE = Locale.US;
private ActivityLifecycleMonitorImpl lifecycleMonitor = new ActivityLifecycleMonitorImpl();
private ExecutorService executorService;
private Handler handlerForMainLooper;
private int startActivityTimeoutSeconds = 45;
private AtomicBoolean anActivityHasBeenLaunched = new AtomicBoolean(false);
private Thread mainThread;
private AtomicLong lastIdleTime = new AtomicLong(0);
private AtomicInteger startedActivityCounter = new AtomicInteger(0);
private IdleHandler idleHandler = new IdleHandler() {
@Override
public boolean queueIdle() {
lastIdleTime.set(System.currentTimeMillis());
return true;
}
};
private volatile boolean finished = false;
private IntentSpy intentSpy = null;
/**
* Sets up intent spying, 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.
*/
@Override
public void onCreate(Bundle arguments) {
Log.i(LOG_TAG, "Instrumentation Started!");
tryLoadingIntentSpy();
InstrumentationRegistry.registerInstance(this);
ActivityLifecycleMonitorRegistry.registerInstance(lifecycleMonitor);
handlerForMainLooper = new Handler(Looper.getMainLooper());
mainThread = Thread.currentThread();
executorService = Executors.newCachedThreadPool();
Looper.myQueue().addIdleHandler(idleHandler);
super.onCreate(arguments);
}
protected final 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());
}
private void tryLoadingIntentSpy() {
// Wouldn't it be easier to call IntentSpyImpl.getInstance() and be done with it? We don't do
// this to avoid bringing in common lib dependencies (which cause painful conflicts with some
// android projects) into G3ITR. Instead, we try to load IntentSpyImpl via reflection. Projects
// that don't have intento in deps will not have IntentSpyImpl included at runtime (i.e. we
// proceed as normally) and leave intentSpy as null (to be checked later). However, if it is
// loaded, we can call any method of IntentSpy.
try {
Class<?> c = Class.forName("com.google.android.apps.common.testing.intento.IntentSpyImpl");
intentSpy = (IntentSpy) c.getMethod(
"load", Context.class, Context.class).invoke(null, getTargetContext(), getContext());
Log.i(LOG_TAG, "IntentSpyImpl loaded");
} catch (ClassNotFoundException cnfe) {
Log.i(LOG_TAG, "IntentSpyImpl not loaded: " + cnfe.getMessage());
} catch (NoSuchMethodException nsme) {
throw new RuntimeException(
"IntentSpyImpl is available at runtime, but getInstance method was not found", nsme);
} catch (SecurityException se) {
throw new RuntimeException(
"IntentSpyImpl is available at runtime, but calling it failed.", se);
} catch (IllegalAccessException iae) {
throw new RuntimeException(
"IntentSpyImpl is available at runtime, but calling it failed.", iae);
} catch (IllegalArgumentException iare) {
throw new RuntimeException(
"IntentSpyImpl is available at runtime, but calling it failed.", iare);
} catch (InvocationTargetException ite) {
throw new RuntimeException(
"IntentSpyImpl is available at runtime, but calling it failed.", ite);
}
}
/**
* 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.
*/
@Override
public void onStart() {
super.onStart();
// 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();
}
/**
* 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.
*/
@Override
public void finish(int resultCode, Bundle results) {
if (finished) {
Log.w(LOG_TAG, "finish called 2x!");
return;
} else {
finished = true;
}
handlerForMainLooper.post(new ActivityFinisher());
long startTime = System.currentTimeMillis();
waitForActivitiesToComplete();
long endTime = System.currentTimeMillis();
Log.i(LOG_TAG, String.format("waitForActivitiesToComplete() took: %sms", endTime - startTime));
ActivityLifecycleMonitorRegistry.registerInstance(null);
super.finish(resultCode, results);
}
/**
* 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.
*/
protected void waitForActivitiesToComplete() {
long endTime = System.currentTimeMillis() + MILLIS_TO_WAIT_FOR_ACTIVITY_TO_STOP;
int currentActivityCount = startedActivityCounter.get();
while (currentActivityCount > 0 && System.currentTimeMillis() < endTime) {
try {
Log.i(LOG_TAG, "Unstopped activity count: " + currentActivityCount);
Thread.sleep(MILLIS_TO_POLL_FOR_ACTIVITY_STOP);
currentActivityCount = startedActivityCounter.get();
} catch (InterruptedException ie) {
Log.i(LOG_TAG, "Abandoning activity wait due to interruption.", ie);
break;
}
}
if (currentActivityCount > 0) {
dumpThreadStateToOutputs("ThreadState-unstopped.txt");
Log.w(LOG_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(LOG_TAG, "Instrumentation Finished!");
Looper.myQueue().removeIdleHandler(idleHandler);
super.onDestroy();
}
@Override
public Activity startActivitySync(final Intent intent) {
validateNotAppThread();
long lastIdleTimeBeforeLaunch = lastIdleTime.get();
if (anActivityHasBeenLaunched.compareAndSet(false, true)) {
// All activities launched from InsturmentationTestCase.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>() {
public Activity call() {
return GoogleInstrumentation.super.startActivitySync(intent);
}
});
try {
return startedActivity.get(startActivityTimeoutSeconds, TimeUnit.SECONDS);
} catch (TimeoutException te) {
startedActivity.cancel(true);
dumpThreadStateToOutputs("ThreadState-startActivityTimeout.txt");
throw new RuntimeException(String.format("Could not launch intent %s within %s seconds. " +
"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 " +
"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, startActivityTimeoutSeconds, 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);
}
}
@Override
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode) {
Log.d(LOG_TAG, "execStartActivity(context, ibinder, ibinder, activity, intent, int)");
if (intentSpy != null) {
intentSpy.record(intent);
if (!intentSpy.allowIntentToProceed(intent)) {
return getResultForBlockedIntent(intent, requestCode);
}
}
return super.execStartActivity(who, contextThread, token, target, intent, requestCode);
}
private ActivityResult getResultForBlockedIntent(Intent intent, int requestCode) {
Log.i(LOG_TAG, "Intento: blocking external intent: " + intent);
if (requestCode >= 0) {
// Caller is expecting result.
ActivityResult result = intentSpy.getActivityResultForIntent(intent);
Log.i(LOG_TAG, "Intento: returning stub result: " + result);
return result;
} else {
return null;
}
}
@Override
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode, Bundle options) {
Log.d(LOG_TAG, "execStartActivity(context, ibinder, ibinder, activity, intent, int, bundle");
if (intentSpy != null) {
intentSpy.record(intent);
if (!intentSpy.allowIntentToProceed(intent)) {
return getResultForBlockedIntent(intent, requestCode);
}
}
return super.execStartActivity(who, contextThread, token, target, intent, requestCode, options);
}
@Override
public void execStartActivities(Context who, IBinder contextThread,
IBinder token, Activity target, Intent[] intents, Bundle options) {
// This method is used in HONEYCOMB and higher 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.
//
// Intento wants to present this back stack for verification, so we record the synthetic
// back stack in the order in which it was constructed.
//
// TODO(user): How appropriate is this if the virtual back stack is for our own application,
// does that even make sense.
Log.d(LOG_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 (int idx = intents.length - 1; idx >= 0; idx--) {
execStartActivity(who, contextThread, token, target, intents[idx], requestCode, options);
}
}
@Override
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, Fragment target,
Intent intent, int requestCode, Bundle options) {
Log.d(LOG_TAG, "execStartActivity(context, IBinder, IBinder, Fragment, Intent, int, Bundle)");
if (intentSpy != null) {
intentSpy.record(intent);
if (!intentSpy.allowIntentToProceed(intent)) {
return getResultForBlockedIntent(intent, requestCode);
}
}
return super.execStartActivity(who, contextThread, token, target, intent, requestCode, options);
}
private void validateNotAppThread() {
if (mainThread.equals(Thread.currentThread())) {
throw new RuntimeException("this method cannot be called from the main application thread");
}
}
@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,
e);
Log.e(LOG_TAG, error);
dumpThreadStateToOutputs("ThreadState-onException.txt");
Log.e(LOG_TAG, "Dying now...");
return super.onException(obj, e);
}
protected final void dumpThreadStateToOutputs(String outputFileName) {
Writer writer = null;
String threadState = getThreadState();
Log.e("THREAD_STATE", threadState);
}
private static 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);
}
/**
* 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<Activity>();
for (Stage s : EnumSet.range(Stage.CREATED, Stage.PAUSED)) {
activities.addAll(lifecycleMonitor.getActivitiesInStage(s));
}
Log.i(LOG_TAG, "Activities that are still in CREATED to PAUSED: " + activities.size());
for (Activity activity : activities) {
if (!activity.isFinishing()) {
try {
Log.i(LOG_TAG, "Stopping activity: " + activity);
activity.finish();
} catch (RuntimeException e) {
Log.e(LOG_TAG, "Failed to stop activity.", e);
}
}
}
}
};
}