AppLaunch.java revision 48a4275bf6281334c1d383701b46f15d8f798063
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 = 60000; //60s 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 incorrectly 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 boolean hasLeanback(Context context) { 180 return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK); 181 } 182 183 private void createMappings() { 184 mNameToIntent = new LinkedHashMap<String, Intent>(); 185 mNameToProcess = new LinkedHashMap<String, String>(); 186 187 PackageManager pm = getInstrumentation().getContext() 188 .getPackageManager(); 189 Intent intentToResolve = new Intent(Intent.ACTION_MAIN); 190 intentToResolve.addCategory(hasLeanback(getInstrumentation().getContext()) ? 191 Intent.CATEGORY_LEANBACK_LAUNCHER : 192 Intent.CATEGORY_LAUNCHER); 193 List<ResolveInfo> ris = pm.queryIntentActivities(intentToResolve, 0); 194 resolveLoop(ris, intentToResolve, pm); 195 // For Wear 196 intentToResolve = new Intent(WEARABLE_ACTION_GOOGLE); 197 ris = pm.queryIntentActivities(intentToResolve, 0); 198 resolveLoop(ris, intentToResolve, pm); 199 } 200 201 private void resolveLoop(List<ResolveInfo> ris, Intent intentToResolve, PackageManager pm) { 202 if (ris == null || ris.isEmpty()) { 203 Log.i(TAG, "Could not find any apps"); 204 } else { 205 for (ResolveInfo ri : ris) { 206 Intent startIntent = new Intent(intentToResolve); 207 startIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK 208 | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); 209 startIntent.setClassName(ri.activityInfo.packageName, 210 ri.activityInfo.name); 211 String appName = ri.loadLabel(pm).toString(); 212 if (appName != null) { 213 mNameToIntent.put(appName, startIntent); 214 mNameToProcess.put(appName, ri.activityInfo.processName); 215 } 216 } 217 } 218 } 219 220 private long startApp(String appName, boolean forceStopBeforeLaunch) 221 throws NameNotFoundException, RemoteException { 222 Log.i(TAG, "Starting " + appName); 223 224 Intent startIntent = mNameToIntent.get(appName); 225 if (startIntent == null) { 226 Log.w(TAG, "App does not exist: " + appName); 227 mResult.putString(mNameToResultKey.get(appName), "App does not exist"); 228 return -1; 229 } 230 AppLaunchRunnable runnable = new AppLaunchRunnable(startIntent, forceStopBeforeLaunch); 231 Thread t = new Thread(runnable); 232 t.start(); 233 try { 234 t.join(JOIN_TIMEOUT); 235 } catch (InterruptedException e) { 236 // ignore 237 } 238 WaitResult result = runnable.getResult(); 239 // report error if any of the following is true: 240 // * launch thread is alive 241 // * result is not null, but: 242 // * result is not START_SUCCESS 243 // * or in case of no force stop, result is not TASK_TO_FRONT either 244 if (t.isAlive() || (result != null 245 && ((result.result != ActivityManager.START_SUCCESS) 246 && (!forceStopBeforeLaunch 247 && result.result != ActivityManager.START_TASK_TO_FRONT)))) { 248 Log.w(TAG, "Assuming app " + appName + " crashed."); 249 reportError(appName, mNameToProcess.get(appName)); 250 return -1; 251 } 252 return result.thisTime; 253 } 254 255 private void checkAccountSignIn() { 256 // ensure that the device has the required account types before starting test 257 // e.g. device must have a valid Google account sign in to measure a meaningful launch time 258 // for Gmail 259 if (mRequiredAccounts == null || mRequiredAccounts.isEmpty()) { 260 return; 261 } 262 final AccountManager am = 263 (AccountManager) getInstrumentation().getTargetContext().getSystemService( 264 Context.ACCOUNT_SERVICE); 265 Account[] accounts = am.getAccounts(); 266 // use set here in case device has multiple accounts of the same type 267 Set<String> foundAccounts = new HashSet<String>(); 268 for (Account account : accounts) { 269 if (mRequiredAccounts.contains(account.type)) { 270 foundAccounts.add(account.type); 271 } 272 } 273 // check if account type matches, if not, fail test with message on what account types 274 // are missing 275 if (mRequiredAccounts.size() != foundAccounts.size()) { 276 mRequiredAccounts.removeAll(foundAccounts); 277 StringBuilder sb = new StringBuilder("Device missing these accounts:"); 278 for (String account : mRequiredAccounts) { 279 sb.append(' '); 280 sb.append(account); 281 } 282 fail(sb.toString()); 283 } 284 } 285 286 private void closeApp(String appName, boolean forceStopApp) { 287 Intent homeIntent = new Intent(Intent.ACTION_MAIN); 288 homeIntent.addCategory(Intent.CATEGORY_HOME); 289 homeIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK 290 | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); 291 getInstrumentation().getContext().startActivity(homeIntent); 292 sleep(POST_LAUNCH_IDLE_TIMEOUT); 293 if (forceStopApp) { 294 Intent startIntent = mNameToIntent.get(appName); 295 if (startIntent != null) { 296 String packageName = startIntent.getComponent().getPackageName(); 297 try { 298 mAm.forceStopPackage(packageName, UserHandle.USER_CURRENT); 299 } catch (RemoteException e) { 300 Log.w(TAG, "Error closing app", e); 301 } 302 } 303 } 304 } 305 306 private void sleep(int time) { 307 try { 308 Thread.sleep(time); 309 } catch (InterruptedException e) { 310 // ignore 311 } 312 } 313 314 private void reportError(String appName, String processName) { 315 ActivityManager am = (ActivityManager) getInstrumentation() 316 .getContext().getSystemService(Context.ACTIVITY_SERVICE); 317 List<ProcessErrorStateInfo> crashes = am.getProcessesInErrorState(); 318 if (crashes != null) { 319 for (ProcessErrorStateInfo crash : crashes) { 320 if (!crash.processName.equals(processName)) 321 continue; 322 323 Log.w(TAG, appName + " crashed: " + crash.shortMsg); 324 mResult.putString(mNameToResultKey.get(appName), crash.shortMsg); 325 return; 326 } 327 } 328 329 mResult.putString(mNameToResultKey.get(appName), 330 "Crashed for unknown reason"); 331 Log.w(TAG, appName 332 + " not found in process list, most likely it is crashed"); 333 } 334 335 private class AppLaunchRunnable implements Runnable { 336 private Intent mLaunchIntent; 337 private IActivityManager.WaitResult mResult; 338 private boolean mForceStopBeforeLaunch; 339 340 public AppLaunchRunnable(Intent intent, boolean forceStopBeforeLaunch) { 341 mLaunchIntent = intent; 342 mForceStopBeforeLaunch = forceStopBeforeLaunch; 343 } 344 345 public IActivityManager.WaitResult getResult() { 346 return mResult; 347 } 348 349 public void run() { 350 try { 351 String packageName = mLaunchIntent.getComponent().getPackageName(); 352 if (mForceStopBeforeLaunch) { 353 mAm.forceStopPackage(packageName, UserHandle.USER_CURRENT); 354 } 355 String mimeType = mLaunchIntent.getType(); 356 if (mimeType == null && mLaunchIntent.getData() != null 357 && "content".equals(mLaunchIntent.getData().getScheme())) { 358 mimeType = mAm.getProviderMimeType(mLaunchIntent.getData(), 359 UserHandle.USER_CURRENT); 360 } 361 362 mResult = mAm.startActivityAndWait(null, null, mLaunchIntent, mimeType, 363 null, null, 0, mLaunchIntent.getFlags(), null, null, 364 UserHandle.USER_CURRENT); 365 } catch (RemoteException e) { 366 Log.w(TAG, "Error launching app", e); 367 } 368 } 369 } 370} 371