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