/*
* Copyright (C) 2013 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 com.android.tests.applaunch;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.app.ActivityManager;
import android.app.ActivityManager.ProcessErrorStateInfo;
import android.app.ActivityManagerNative;
import android.app.IActivityManager;
import android.app.IActivityManager.WaitResult;
import android.app.UiAutomation;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ResolveInfo;
import android.os.Bundle;
import android.os.RemoteException;
import android.os.UserHandle;
import android.test.InstrumentationTestCase;
import android.test.InstrumentationTestRunner;
import android.util.Log;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* This test is intended to measure the time it takes for the apps to start.
* Names of the applications are passed in command line, and the
* test starts each application, and reports the start up time in milliseconds.
* The instrumentation expects the following key to be passed on the command line:
* apps - A list of applications to start and their corresponding result keys
* in the following format:
* -e apps <app name>^<result key>|<app name>^<result key>
*/
public class AppLaunch extends InstrumentationTestCase {
private static final int JOIN_TIMEOUT = 10000;
private static final String TAG = AppLaunch.class.getSimpleName();
private static final String KEY_APPS = "apps";
private static final String KEY_LAUNCH_ITERATIONS = "launch_iterations";
// optional parameter: comma separated list of required account types before proceeding
// with the app launch
private static final String KEY_REQUIRED_ACCOUNTS = "required_accounts";
private static final int INITIAL_LAUNCH_IDLE_TIMEOUT = 7500; //7.5s to allow app to idle
private static final int POST_LAUNCH_IDLE_TIMEOUT = 750; //750ms idle for non initial launches
private static final int BETWEEN_LAUNCH_SLEEP_TIMEOUT = 2000; //2s between launching apps
private Map<String, Intent> mNameToIntent;
private Map<String, String> mNameToProcess;
private Map<String, String> mNameToResultKey;
private Map<String, Long> mNameToLaunchTime;
private IActivityManager mAm;
private int mLaunchIterations = 10;
private Bundle mResult = new Bundle();
private Set<String> mRequiredAccounts;
@Override
protected void setUp() throws Exception {
super.setUp();
getInstrumentation().getUiAutomation().setRotation(UiAutomation.ROTATION_FREEZE_0);
}
@Override
protected void tearDown() throws Exception {
getInstrumentation().getUiAutomation().setRotation(UiAutomation.ROTATION_UNFREEZE);
super.tearDown();
}
public void testMeasureStartUpTime() throws RemoteException, NameNotFoundException {
InstrumentationTestRunner instrumentation =
(InstrumentationTestRunner)getInstrumentation();
Bundle args = instrumentation.getArguments();
mAm = ActivityManagerNative.getDefault();
createMappings();
parseArgs(args);
checkAccountSignIn();
// do initial app launch, without force stopping
for (String app : mNameToResultKey.keySet()) {
long launchTime = startApp(app, false);
if (launchTime <= 0) {
mNameToLaunchTime.put(app, -1L);
// simply pass the app if launch isn't successful
// error should have already been logged by startApp
continue;
} else {
mNameToLaunchTime.put(app, launchTime);
}
sleep(INITIAL_LAUNCH_IDLE_TIMEOUT);
closeApp(app, false);
sleep(BETWEEN_LAUNCH_SLEEP_TIMEOUT);
}
// do the real app launch now
for (int i = 0; i < mLaunchIterations; i++) {
for (String app : mNameToResultKey.keySet()) {
long prevLaunchTime = mNameToLaunchTime.get(app);
long launchTime = 0;
if (prevLaunchTime < 0) {
// skip if the app has previous failures
continue;
}
launchTime = startApp(app, true);
if (launchTime <= 0) {
// if it fails once, skip the rest of the launches
mNameToLaunchTime.put(app, -1L);
continue;
}
// keep the min launch time
if (launchTime < prevLaunchTime) {
mNameToLaunchTime.put(app, launchTime);
}
sleep(POST_LAUNCH_IDLE_TIMEOUT);
closeApp(app, true);
sleep(BETWEEN_LAUNCH_SLEEP_TIMEOUT);
}
}
for (String app : mNameToResultKey.keySet()) {
long launchTime = mNameToLaunchTime.get(app);
if (launchTime != -1) {
mResult.putLong(mNameToResultKey.get(app), launchTime);
}
}
instrumentation.sendStatus(0, mResult);
}
private void parseArgs(Bundle args) {
mNameToResultKey = new LinkedHashMap<String, String>();
mNameToLaunchTime = new HashMap<String, Long>();
String launchIterations = args.getString(KEY_LAUNCH_ITERATIONS);
if (launchIterations != null) {
mLaunchIterations = Integer.parseInt(launchIterations);
}
String appList = args.getString(KEY_APPS);
if (appList == null)
return;
String appNames[] = appList.split("\\|");
for (String pair : appNames) {
String[] parts = pair.split("\\^");
if (parts.length != 2) {
Log.e(TAG, "The apps key is incorectly formatted");
fail();
}
mNameToResultKey.put(parts[0], parts[1]);
mNameToLaunchTime.put(parts[0], 0L);
}
String requiredAccounts = args.getString(KEY_REQUIRED_ACCOUNTS);
if (requiredAccounts != null) {
mRequiredAccounts = new HashSet<String>();
for (String accountType : requiredAccounts.split(",")) {
mRequiredAccounts.add(accountType);
}
}
}
private void createMappings() {
mNameToIntent = new LinkedHashMap<String, Intent>();
mNameToProcess = new LinkedHashMap<String, String>();
PackageManager pm = getInstrumentation().getContext()
.getPackageManager();
Intent intentToResolve = new Intent(Intent.ACTION_MAIN);
intentToResolve.addCategory(Intent.CATEGORY_LAUNCHER);
List<ResolveInfo> ris = pm.queryIntentActivities(intentToResolve, 0);
if (ris == null || ris.isEmpty()) {
Log.i(TAG, "Could not find any apps");
} else {
for (ResolveInfo ri : ris) {
Intent startIntent = new Intent(intentToResolve);
startIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
startIntent.setClassName(ri.activityInfo.packageName,
ri.activityInfo.name);
String appName = ri.loadLabel(pm).toString();
if (appName != null) {
mNameToIntent.put(appName, startIntent);
mNameToProcess.put(appName, ri.activityInfo.processName);
}
}
}
}
private long startApp(String appName, boolean forceStopBeforeLaunch)
throws NameNotFoundException, RemoteException {
Log.i(TAG, "Starting " + appName);
Intent startIntent = mNameToIntent.get(appName);
if (startIntent == null) {
Log.w(TAG, "App does not exist: " + appName);
mResult.putString(mNameToResultKey.get(appName), "App does not exist");
return -1;
}
AppLaunchRunnable runnable = new AppLaunchRunnable(startIntent, forceStopBeforeLaunch);
Thread t = new Thread(runnable);
t.start();
try {
t.join(JOIN_TIMEOUT);
} catch (InterruptedException e) {
// ignore
}
WaitResult result = runnable.getResult();
// report error if any of the following is true:
// * launch thread is alive
// * result is not null, but:
// * result is not START_SUCESS
// * or in case of no force stop, result is not TASK_TO_FRONT either
if (t.isAlive() || (result != null
&& ((result.result != ActivityManager.START_SUCCESS)
&& (!forceStopBeforeLaunch
&& result.result != ActivityManager.START_TASK_TO_FRONT)))) {
Log.w(TAG, "Assuming app " + appName + " crashed.");
reportError(appName, mNameToProcess.get(appName));
return -1;
}
return result.thisTime;
}
private void checkAccountSignIn() {
// ensure that the device has the required account types before starting test
// e.g. device must have a valid Google account sign in to measure a meaningful launch time
// for Gmail
if (mRequiredAccounts == null || mRequiredAccounts.isEmpty()) {
return;
}
final AccountManager am =
(AccountManager) getInstrumentation().getTargetContext().getSystemService(
Context.ACCOUNT_SERVICE);
Account[] accounts = am.getAccounts();
// use set here in case device has multiple accounts of the same type
Set<String> foundAccounts = new HashSet<String>();
for (Account account : accounts) {
if (mRequiredAccounts.contains(account.type)) {
foundAccounts.add(account.type);
}
}
// check if account type matches, if not, fail test with message on what account types
// are missing
if (mRequiredAccounts.size() != foundAccounts.size()) {
mRequiredAccounts.removeAll(foundAccounts);
StringBuilder sb = new StringBuilder("Device missing these accounts:");
for (String account : mRequiredAccounts) {
sb.append(' ');
sb.append(account);
}
fail(sb.toString());
}
}
private void closeApp(String appName, boolean forceStopApp) {
Intent homeIntent = new Intent(Intent.ACTION_MAIN);
homeIntent.addCategory(Intent.CATEGORY_HOME);
homeIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
getInstrumentation().getContext().startActivity(homeIntent);
sleep(POST_LAUNCH_IDLE_TIMEOUT);
if (forceStopApp) {
Intent startIntent = mNameToIntent.get(appName);
if (startIntent != null) {
String packageName = startIntent.getComponent().getPackageName();
try {
mAm.forceStopPackage(packageName, UserHandle.USER_CURRENT);
} catch (RemoteException e) {
Log.w(TAG, "Error closing app", e);
}
}
}
}
private void sleep(int time) {
try {
Thread.sleep(time);
} catch (InterruptedException e) {
// ignore
}
}
private void reportError(String appName, String processName) {
ActivityManager am = (ActivityManager) getInstrumentation()
.getContext().getSystemService(Context.ACTIVITY_SERVICE);
List<ProcessErrorStateInfo> crashes = am.getProcessesInErrorState();
if (crashes != null) {
for (ProcessErrorStateInfo crash : crashes) {
if (!crash.processName.equals(processName))
continue;
Log.w(TAG, appName + " crashed: " + crash.shortMsg);
mResult.putString(mNameToResultKey.get(appName), crash.shortMsg);
return;
}
}
mResult.putString(mNameToResultKey.get(appName),
"Crashed for unknown reason");
Log.w(TAG, appName
+ " not found in process list, most likely it is crashed");
}
private class AppLaunchRunnable implements Runnable {
private Intent mLaunchIntent;
private IActivityManager.WaitResult mResult;
private boolean mForceStopBeforeLaunch;
public AppLaunchRunnable(Intent intent, boolean forceStopBeforeLaunch) {
mLaunchIntent = intent;
mForceStopBeforeLaunch = forceStopBeforeLaunch;
}
public IActivityManager.WaitResult getResult() {
return mResult;
}
public void run() {
try {
String packageName = mLaunchIntent.getComponent().getPackageName();
if (mForceStopBeforeLaunch) {
mAm.forceStopPackage(packageName, UserHandle.USER_CURRENT);
}
String mimeType = mLaunchIntent.getType();
if (mimeType == null && mLaunchIntent.getData() != null
&& "content".equals(mLaunchIntent.getData().getScheme())) {
mimeType = mAm.getProviderMimeType(mLaunchIntent.getData(),
UserHandle.USER_CURRENT);
}
mResult = mAm.startActivityAndWait(null, null, mLaunchIntent, mimeType,
null, null, 0, mLaunchIntent.getFlags(), null, null,
UserHandle.USER_CURRENT);
} catch (RemoteException e) {
Log.w(TAG, "Error launching app", e);
}
}
}
}