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