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