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