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