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