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