AppLaunch.java revision e2a03fe43d1f8eb5a6ada046870dcc35392d8f17
1/* 2 * Copyright (C) 2013 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16package com.android.tests.applaunch; 17 18import android.accounts.Account; 19import android.accounts.AccountManager; 20import android.app.ActivityManager; 21import android.app.ActivityManager.ProcessErrorStateInfo; 22import android.app.ActivityManagerNative; 23import android.app.IActivityManager; 24import android.app.IActivityManager.WaitResult; 25import android.app.UiAutomation; 26import android.content.Context; 27import android.content.Intent; 28import android.content.pm.PackageManager; 29import android.content.pm.PackageManager.NameNotFoundException; 30import android.content.pm.ResolveInfo; 31import android.os.Bundle; 32import android.os.RemoteException; 33import android.os.UserHandle; 34import android.test.InstrumentationTestCase; 35import android.test.InstrumentationTestRunner; 36import android.util.Log; 37 38import java.util.HashMap; 39import java.util.HashSet; 40import java.util.LinkedHashMap; 41import java.util.List; 42import java.util.Map; 43import java.util.Set; 44 45/** 46 * This test is intended to measure the time it takes for the apps to start. 47 * Names of the applications are passed in command line, and the 48 * test starts each application, and reports the start up time in milliseconds. 49 * The instrumentation expects the following key to be passed on the command line: 50 * apps - A list of applications to start and their corresponding result keys 51 * in the following format: 52 * -e apps <app name>^<result key>|<app name>^<result key> 53 */ 54public class AppLaunch extends InstrumentationTestCase { 55 56 private static final int JOIN_TIMEOUT = 10000; 57 private static final String TAG = AppLaunch.class.getSimpleName(); 58 private static final String KEY_APPS = "apps"; 59 private static final String KEY_LAUNCH_ITERATIONS = "launch_iterations"; 60 // optional parameter: comma separated list of required account types before proceeding 61 // with the app launch 62 private static final String KEY_REQUIRED_ACCOUNTS = "required_accounts"; 63 private static final int INITIAL_LAUNCH_IDLE_TIMEOUT = 7500; //7.5s to allow app to idle 64 private static final int POST_LAUNCH_IDLE_TIMEOUT = 750; //750ms idle for non initial launches 65 private static final int BETWEEN_LAUNCH_SLEEP_TIMEOUT = 2000; //2s between launching apps 66 67 private Map<String, Intent> mNameToIntent; 68 private Map<String, String> mNameToProcess; 69 private Map<String, String> mNameToResultKey; 70 private Map<String, Long> mNameToLaunchTime; 71 private IActivityManager mAm; 72 private int mLaunchIterations = 10; 73 private Bundle mResult = new Bundle(); 74 private Set<String> mRequiredAccounts; 75 76 @Override 77 protected void setUp() throws Exception { 78 super.setUp(); 79 getInstrumentation().getUiAutomation().setRotation(UiAutomation.ROTATION_FREEZE_0); 80 } 81 82 @Override 83 protected void tearDown() throws Exception { 84 getInstrumentation().getUiAutomation().setRotation(UiAutomation.ROTATION_UNFREEZE); 85 super.tearDown(); 86 } 87 88 public void testMeasureStartUpTime() throws RemoteException, NameNotFoundException { 89 InstrumentationTestRunner instrumentation = 90 (InstrumentationTestRunner)getInstrumentation(); 91 Bundle args = instrumentation.getArguments(); 92 mAm = ActivityManagerNative.getDefault(); 93 94 createMappings(); 95 parseArgs(args); 96 checkAccountSignIn(); 97 98 // do initial app launch, without force stopping 99 for (String app : mNameToResultKey.keySet()) { 100 long launchTime = startApp(app, false); 101 if (launchTime <= 0) { 102 mNameToLaunchTime.put(app, -1L); 103 // simply pass the app if launch isn't successful 104 // error should have already been logged by startApp 105 continue; 106 } else { 107 mNameToLaunchTime.put(app, launchTime); 108 } 109 sleep(INITIAL_LAUNCH_IDLE_TIMEOUT); 110 closeApp(app, false); 111 sleep(BETWEEN_LAUNCH_SLEEP_TIMEOUT); 112 } 113 // do the real app launch now 114 for (int i = 0; i < mLaunchIterations; i++) { 115 for (String app : mNameToResultKey.keySet()) { 116 long prevLaunchTime = mNameToLaunchTime.get(app); 117 long launchTime = 0; 118 if (prevLaunchTime < 0) { 119 // skip if the app has previous failures 120 continue; 121 } 122 launchTime = startApp(app, true); 123 if (launchTime <= 0) { 124 // if it fails once, skip the rest of the launches 125 mNameToLaunchTime.put(app, -1L); 126 continue; 127 } 128 // keep the min launch time 129 if (launchTime < prevLaunchTime) { 130 mNameToLaunchTime.put(app, launchTime); 131 } 132 sleep(POST_LAUNCH_IDLE_TIMEOUT); 133 closeApp(app, true); 134 sleep(BETWEEN_LAUNCH_SLEEP_TIMEOUT); 135 } 136 } 137 for (String app : mNameToResultKey.keySet()) { 138 long launchTime = mNameToLaunchTime.get(app); 139 if (launchTime != -1) { 140 mResult.putLong(mNameToResultKey.get(app), launchTime); 141 } 142 } 143 instrumentation.sendStatus(0, mResult); 144 } 145 146 private void parseArgs(Bundle args) { 147 mNameToResultKey = new LinkedHashMap<String, String>(); 148 mNameToLaunchTime = new HashMap<String, Long>(); 149 String launchIterations = args.getString(KEY_LAUNCH_ITERATIONS); 150 if (launchIterations != null) { 151 mLaunchIterations = Integer.parseInt(launchIterations); 152 } 153 String appList = args.getString(KEY_APPS); 154 if (appList == null) 155 return; 156 157 String appNames[] = appList.split("\\|"); 158 for (String pair : appNames) { 159 String[] parts = pair.split("\\^"); 160 if (parts.length != 2) { 161 Log.e(TAG, "The apps key is incorectly formatted"); 162 fail(); 163 } 164 165 mNameToResultKey.put(parts[0], parts[1]); 166 mNameToLaunchTime.put(parts[0], 0L); 167 } 168 String requiredAccounts = args.getString(KEY_REQUIRED_ACCOUNTS); 169 if (requiredAccounts != null) { 170 mRequiredAccounts = new HashSet<String>(); 171 for (String accountType : requiredAccounts.split(",")) { 172 mRequiredAccounts.add(accountType); 173 } 174 } 175 } 176 177 private void createMappings() { 178 mNameToIntent = new LinkedHashMap<String, Intent>(); 179 mNameToProcess = new LinkedHashMap<String, String>(); 180 181 PackageManager pm = getInstrumentation().getContext() 182 .getPackageManager(); 183 Intent intentToResolve = new Intent(Intent.ACTION_MAIN); 184 intentToResolve.addCategory(Intent.CATEGORY_LAUNCHER); 185 List<ResolveInfo> ris = pm.queryIntentActivities(intentToResolve, 0); 186 if (ris == null || ris.isEmpty()) { 187 Log.i(TAG, "Could not find any apps"); 188 } else { 189 for (ResolveInfo ri : ris) { 190 Intent startIntent = new Intent(intentToResolve); 191 startIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK 192 | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); 193 startIntent.setClassName(ri.activityInfo.packageName, 194 ri.activityInfo.name); 195 String appName = ri.loadLabel(pm).toString(); 196 if (appName != null) { 197 mNameToIntent.put(appName, startIntent); 198 mNameToProcess.put(appName, ri.activityInfo.processName); 199 } 200 } 201 } 202 } 203 204 private long startApp(String appName, boolean forceStopBeforeLaunch) 205 throws NameNotFoundException, RemoteException { 206 Log.i(TAG, "Starting " + appName); 207 208 Intent startIntent = mNameToIntent.get(appName); 209 if (startIntent == null) { 210 Log.w(TAG, "App does not exist: " + appName); 211 mResult.putString(mNameToResultKey.get(appName), "App does not exist"); 212 return -1; 213 } 214 AppLaunchRunnable runnable = new AppLaunchRunnable(startIntent, forceStopBeforeLaunch); 215 Thread t = new Thread(runnable); 216 t.start(); 217 try { 218 t.join(JOIN_TIMEOUT); 219 } catch (InterruptedException e) { 220 // ignore 221 } 222 WaitResult result = runnable.getResult(); 223 // report error if any of the following is true: 224 // * launch thread is alive 225 // * result is not null, but: 226 // * result is not START_SUCESS 227 // * or in case of no force stop, result is not TASK_TO_FRONT either 228 if (t.isAlive() || (result != null 229 && ((result.result != ActivityManager.START_SUCCESS) 230 && (!forceStopBeforeLaunch 231 && result.result != ActivityManager.START_TASK_TO_FRONT)))) { 232 Log.w(TAG, "Assuming app " + appName + " crashed."); 233 reportError(appName, mNameToProcess.get(appName)); 234 return -1; 235 } 236 return result.thisTime; 237 } 238 239 private void checkAccountSignIn() { 240 // ensure that the device has the required account types before starting test 241 // e.g. device must have a valid Google account sign in to measure a meaningful launch time 242 // for Gmail 243 if (mRequiredAccounts == null || mRequiredAccounts.isEmpty()) { 244 return; 245 } 246 final AccountManager am = 247 (AccountManager) getInstrumentation().getTargetContext().getSystemService( 248 Context.ACCOUNT_SERVICE); 249 Account[] accounts = am.getAccounts(); 250 // use set here in case device has multiple accounts of the same type 251 Set<String> foundAccounts = new HashSet<String>(); 252 for (Account account : accounts) { 253 if (mRequiredAccounts.contains(account.type)) { 254 foundAccounts.add(account.type); 255 } 256 } 257 // check if account type matches, if not, fail test with message on what account types 258 // are missing 259 if (mRequiredAccounts.size() != foundAccounts.size()) { 260 mRequiredAccounts.removeAll(foundAccounts); 261 StringBuilder sb = new StringBuilder("Device missing these accounts:"); 262 for (String account : mRequiredAccounts) { 263 sb.append(' '); 264 sb.append(account); 265 } 266 fail(sb.toString()); 267 } 268 } 269 270 private void closeApp(String appName, boolean forceStopApp) { 271 Intent homeIntent = new Intent(Intent.ACTION_MAIN); 272 homeIntent.addCategory(Intent.CATEGORY_HOME); 273 homeIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK 274 | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); 275 getInstrumentation().getContext().startActivity(homeIntent); 276 sleep(POST_LAUNCH_IDLE_TIMEOUT); 277 if (forceStopApp) { 278 Intent startIntent = mNameToIntent.get(appName); 279 if (startIntent != null) { 280 String packageName = startIntent.getComponent().getPackageName(); 281 try { 282 mAm.forceStopPackage(packageName, UserHandle.USER_CURRENT); 283 } catch (RemoteException e) { 284 Log.w(TAG, "Error closing app", e); 285 } 286 } 287 } 288 } 289 290 private void sleep(int time) { 291 try { 292 Thread.sleep(time); 293 } catch (InterruptedException e) { 294 // ignore 295 } 296 } 297 298 private void reportError(String appName, String processName) { 299 ActivityManager am = (ActivityManager) getInstrumentation() 300 .getContext().getSystemService(Context.ACTIVITY_SERVICE); 301 List<ProcessErrorStateInfo> crashes = am.getProcessesInErrorState(); 302 if (crashes != null) { 303 for (ProcessErrorStateInfo crash : crashes) { 304 if (!crash.processName.equals(processName)) 305 continue; 306 307 Log.w(TAG, appName + " crashed: " + crash.shortMsg); 308 mResult.putString(mNameToResultKey.get(appName), crash.shortMsg); 309 return; 310 } 311 } 312 313 mResult.putString(mNameToResultKey.get(appName), 314 "Crashed for unknown reason"); 315 Log.w(TAG, appName 316 + " not found in process list, most likely it is crashed"); 317 } 318 319 private class AppLaunchRunnable implements Runnable { 320 private Intent mLaunchIntent; 321 private IActivityManager.WaitResult mResult; 322 private boolean mForceStopBeforeLaunch; 323 324 public AppLaunchRunnable(Intent intent, boolean forceStopBeforeLaunch) { 325 mLaunchIntent = intent; 326 mForceStopBeforeLaunch = forceStopBeforeLaunch; 327 } 328 329 public IActivityManager.WaitResult getResult() { 330 return mResult; 331 } 332 333 public void run() { 334 try { 335 String packageName = mLaunchIntent.getComponent().getPackageName(); 336 if (mForceStopBeforeLaunch) { 337 mAm.forceStopPackage(packageName, UserHandle.USER_CURRENT); 338 } 339 String mimeType = mLaunchIntent.getType(); 340 if (mimeType == null && mLaunchIntent.getData() != null 341 && "content".equals(mLaunchIntent.getData().getScheme())) { 342 mimeType = mAm.getProviderMimeType(mLaunchIntent.getData(), 343 UserHandle.USER_CURRENT); 344 } 345 346 mResult = mAm.startActivityAndWait(null, null, mLaunchIntent, mimeType, 347 null, null, 0, mLaunchIntent.getFlags(), null, null, null, 348 UserHandle.USER_CURRENT); 349 } catch (RemoteException e) { 350 Log.w(TAG, "Error launching app", e); 351 } 352 } 353 } 354} 355