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 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
81    protected void setUp() throws Exception {
82        super.setUp();
83        getInstrumentation().getUiAutomation().setRotation(UiAutomation.ROTATION_FREEZE_0);
84    }
85
86    @Override
87    protected void tearDown() throws Exception {
88        getInstrumentation().getUiAutomation().setRotation(UiAutomation.ROTATION_UNFREEZE);
89        super.tearDown();
90    }
91
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
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
184    private boolean hasLeanback(Context context) {
185        return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK);
186    }
187
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
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
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
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
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
311    private void sleep(int time) {
312        try {
313            Thread.sleep(time);
314        } catch (InterruptedException e) {
315            // ignore
316        }
317    }
318
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
345        public AppLaunchRunnable(Intent intent, boolean forceStopBeforeLaunch) {
346            mLaunchIntent = intent;
347            mForceStopBeforeLaunch = forceStopBeforeLaunch;
348        }
349
350        public IActivityManager.WaitResult getResult() {
351            return mResult;
352        }
353
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