AppLaunch.java revision 6266a436f35622f3b48de8d7b245bb5f8a4c567d
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