• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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  */
16 package com.android.tests.applaunch;
17 
18 import android.accounts.Account;
19 import android.accounts.AccountManager;
20 import android.app.ActivityManager;
21 import android.app.ActivityManager.ProcessErrorStateInfo;
22 import android.app.ActivityManagerNative;
23 import android.app.IActivityManager;
24 import android.app.IActivityManager.WaitResult;
25 import android.app.UiAutomation;
26 import android.content.Context;
27 import android.content.Intent;
28 import android.content.pm.PackageManager;
29 import android.content.pm.PackageManager.NameNotFoundException;
30 import android.content.pm.ResolveInfo;
31 import android.os.Bundle;
32 import android.os.RemoteException;
33 import android.os.UserHandle;
34 import android.test.InstrumentationTestCase;
35 import android.test.InstrumentationTestRunner;
36 import android.util.Log;
37 
38 import java.util.HashMap;
39 import java.util.HashSet;
40 import java.util.LinkedHashMap;
41 import java.util.List;
42 import java.util.Map;
43 import 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  */
54 public 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
setUp()81     protected void setUp() throws Exception {
82         super.setUp();
83         getInstrumentation().getUiAutomation().setRotation(UiAutomation.ROTATION_FREEZE_0);
84     }
85 
86     @Override
tearDown()87     protected void tearDown() throws Exception {
88         getInstrumentation().getUiAutomation().setRotation(UiAutomation.ROTATION_UNFREEZE);
89         super.tearDown();
90     }
91 
testMeasureStartUpTime()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 
parseArgs(Bundle args)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 
hasLeanback(Context context)184     private boolean hasLeanback(Context context) {
185         return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK);
186     }
187 
createMappings()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 
resolveLoop(List<ResolveInfo> ris, Intent intentToResolve, PackageManager pm)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 
startApp(String appName, boolean forceStopBeforeLaunch)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 
checkAccountSignIn()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 
closeApp(String appName, boolean forceStopApp)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 
sleep(int time)311     private void sleep(int time) {
312         try {
313             Thread.sleep(time);
314         } catch (InterruptedException e) {
315             // ignore
316         }
317     }
318 
reportError(String appName, String processName)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 
AppLaunchRunnable(Intent intent, boolean forceStopBeforeLaunch)345         public AppLaunchRunnable(Intent intent, boolean forceStopBeforeLaunch) {
346             mLaunchIntent = intent;
347             mForceStopBeforeLaunch = forceStopBeforeLaunch;
348         }
349 
getResult()350         public IActivityManager.WaitResult getResult() {
351             return mResult;
352         }
353 
run()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