• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2012 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  */
16 
17 package com.android.compatibilitytest;
18 
19 import android.app.ActivityManager;
20 import android.app.ActivityManager.ProcessErrorStateInfo;
21 import android.app.ActivityManager.RunningTaskInfo;
22 import android.app.IActivityController;
23 import android.app.IActivityManager;
24 import android.app.Instrumentation;
25 import android.app.UiAutomation;
26 import android.app.UiModeManager;
27 import android.content.Context;
28 import android.content.Intent;
29 import android.content.pm.PackageManager;
30 import android.content.pm.ResolveInfo;
31 import android.content.res.Configuration;
32 import android.os.Bundle;
33 import android.os.DropBoxManager;
34 import android.os.RemoteException;
35 import android.os.ServiceManager;
36 import android.support.test.InstrumentationRegistry;
37 import android.support.test.runner.AndroidJUnit4;
38 import android.util.Log;
39 
40 import org.junit.After;
41 import org.junit.Assert;
42 import org.junit.Before;
43 import org.junit.Test;
44 import org.junit.runner.RunWith;
45 
46 import java.util.ArrayList;
47 import java.util.Collection;
48 import java.util.HashMap;
49 import java.util.HashSet;
50 import java.util.List;
51 import java.util.Map;
52 import java.util.Set;
53 
54 /**
55  * Application Compatibility Test that launches an application and detects
56  * crashes.
57  */
58 @RunWith(AndroidJUnit4.class)
59 public class AppCompatibility {
60 
61     private static final String TAG = AppCompatibility.class.getSimpleName();
62     private static final String PACKAGE_TO_LAUNCH = "package_to_launch";
63     private static final String APP_LAUNCH_TIMEOUT_MSECS = "app_launch_timeout_ms";
64     private static final String WORKSPACE_LAUNCH_TIMEOUT_MSECS = "workspace_launch_timeout_ms";
65     private static final Set<String> DROPBOX_TAGS = new HashSet<>();
66     static {
67         DROPBOX_TAGS.add("SYSTEM_TOMBSTONE");
68         DROPBOX_TAGS.add("system_app_anr");
69         DROPBOX_TAGS.add("system_app_native_crash");
70         DROPBOX_TAGS.add("system_app_crash");
71         DROPBOX_TAGS.add("data_app_anr");
72         DROPBOX_TAGS.add("data_app_native_crash");
73         DROPBOX_TAGS.add("data_app_crash");
74     }
75     private static final int MAX_CRASH_SNIPPET_LINES = 20;
76     private static final int MAX_NUM_CRASH_SNIPPET = 3;
77 
78     // time waiting for app to launch
79     private int mAppLaunchTimeout = 7000;
80     // time waiting for launcher home screen to show up
81     private int mWorkspaceLaunchTimeout = 2000;
82 
83     private Context mContext;
84     private ActivityManager mActivityManager;
85     private PackageManager mPackageManager;
86     private Bundle mArgs;
87     private Instrumentation mInstrumentation;
88     private String mLauncherPackageName;
89     private IActivityController mCrashSupressor = new CrashSuppressor();
90     private Map<String, List<String>> mAppErrors = new HashMap<>();
91 
92     @Before
setUp()93     public void setUp() throws Exception {
94         mInstrumentation = InstrumentationRegistry.getInstrumentation();
95         mContext = InstrumentationRegistry.getTargetContext();
96         mActivityManager = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE);
97         mPackageManager = mContext.getPackageManager();
98         mArgs = InstrumentationRegistry.getArguments();
99 
100         // resolve launcher package name
101         Intent intent = new Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME);
102         ResolveInfo resolveInfo = mPackageManager.resolveActivity(
103                 intent, PackageManager.MATCH_DEFAULT_ONLY);
104         mLauncherPackageName = resolveInfo.activityInfo.packageName;
105         Assert.assertNotNull("failed to resolve package name for launcher", mLauncherPackageName);
106         Log.v(TAG, "Using launcher package name: " + mLauncherPackageName);
107 
108         // Parse optional inputs.
109         String appLaunchTimeoutMsecs = mArgs.getString(APP_LAUNCH_TIMEOUT_MSECS);
110         if (appLaunchTimeoutMsecs != null) {
111             mAppLaunchTimeout = Integer.parseInt(appLaunchTimeoutMsecs);
112         }
113         String workspaceLaunchTimeoutMsecs = mArgs.getString(WORKSPACE_LAUNCH_TIMEOUT_MSECS);
114         if (workspaceLaunchTimeoutMsecs != null) {
115             mWorkspaceLaunchTimeout = Integer.parseInt(workspaceLaunchTimeoutMsecs);
116         }
117         mInstrumentation.getUiAutomation().setRotation(UiAutomation.ROTATION_FREEZE_0);
118 
119         // set activity controller to suppress crash dialogs and collects them by process name
120         mAppErrors.clear();
121         IActivityManager.Stub.asInterface(ServiceManager.checkService(Context.ACTIVITY_SERVICE))
122             .setActivityController(mCrashSupressor, false);
123     }
124 
125     @After
tearDown()126     public void tearDown() throws Exception {
127         // unset activity controller
128         IActivityManager.Stub.asInterface(ServiceManager.checkService(Context.ACTIVITY_SERVICE))
129             .setActivityController(null, false);
130         mInstrumentation.getUiAutomation().setRotation(UiAutomation.ROTATION_UNFREEZE);
131     }
132 
133     /**
134      * Actual test case that launches the package and throws an exception on the
135      * first error.
136      *
137      * @throws Exception
138      */
139     @Test
testAppStability()140     public void testAppStability() throws Exception {
141         String packageName = mArgs.getString(PACKAGE_TO_LAUNCH);
142         if (packageName != null) {
143             Log.d(TAG, "Launching app " + packageName);
144             Intent intent = getLaunchIntentForPackage(packageName);
145             if (intent == null) {
146                 Log.w(TAG, String.format("Skipping %s; no launch intent", packageName));
147                 return;
148             }
149             long startTime = System.currentTimeMillis();
150             launchActivity(packageName, intent);
151             try {
152                 checkDropbox(startTime, packageName);
153                 if (mAppErrors.containsKey(packageName)) {
154                     StringBuilder message = new StringBuilder("Error(s) detected for package: ")
155                             .append(packageName);
156                     List<String> errors = mAppErrors.get(packageName);
157                     for (int i = 0; i < MAX_NUM_CRASH_SNIPPET && i < errors.size(); i++) {
158                         String err = errors.get(i);
159                         message.append("\n\n");
160                         // limit the size of each crash snippet
161                         message.append(truncate(err, MAX_CRASH_SNIPPET_LINES));
162                     }
163                     if (errors.size() > MAX_NUM_CRASH_SNIPPET) {
164                         message.append(String.format("\n... %d more errors omitted ...",
165                                 errors.size() - MAX_NUM_CRASH_SNIPPET));
166                     }
167                     Assert.fail(message.toString());
168                 }
169                 // last check: see if app process is still running
170                 Assert.assertTrue("app package \"" + packageName + "\" no longer found in running "
171                     + "tasks, but no explicit crashes were detected; check logcat for details",
172                     processStillUp(packageName));
173             } finally {
174                 returnHome();
175             }
176         } else {
177             Log.d(TAG, "Missing argument, use " + PACKAGE_TO_LAUNCH +
178                     " to specify the package to launch");
179         }
180     }
181 
182     /**
183      * Truncate the text to at most the specified number of lines, and append a marker at the end
184      * when truncated
185      * @param text
186      * @param maxLines
187      * @return
188      */
truncate(String text, int maxLines)189     private static String truncate(String text, int maxLines) {
190         String[] lines = text.split("\\r?\\n");
191         StringBuilder ret = new StringBuilder();
192         for (int i = 0; i < maxLines && i < lines.length; i++) {
193             ret.append(lines[i]);
194             ret.append('\n');
195         }
196         if (lines.length > maxLines) {
197             ret.append("... ");
198             ret.append(lines.length - maxLines);
199             ret.append(" more lines truncated ...\n");
200         }
201         return ret.toString();
202     }
203 
204     /**
205      * Check dropbox for entries of interest regarding the specified process
206      * @param startTime if not 0, only check entries with timestamp later than the start time
207      * @param processName the process name to check for
208      */
checkDropbox(long startTime, String processName)209     private void checkDropbox(long startTime, String processName) {
210         DropBoxManager dropbox = (DropBoxManager) mContext
211                 .getSystemService(Context.DROPBOX_SERVICE);
212         DropBoxManager.Entry entry = null;
213         while (null != (entry = dropbox.getNextEntry(null, startTime))) {
214             try {
215                 // only check entries with tag that's of interest
216                 String tag = entry.getTag();
217                 if (DROPBOX_TAGS.contains(tag)) {
218                     String content = entry.getText(4096);
219                     if (content != null) {
220                         if (content.contains(processName)) {
221                             addProcessError(processName, "dropbox:" + tag, content);
222                         }
223                     }
224                 }
225                 startTime = entry.getTimeMillis();
226             } finally {
227                 entry.close();
228             }
229         }
230     }
231 
returnHome()232     private void returnHome() {
233         Intent homeIntent = new Intent(Intent.ACTION_MAIN);
234         homeIntent.addCategory(Intent.CATEGORY_HOME);
235         homeIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
236         // Send the "home" intent and wait 2 seconds for us to get there
237         mContext.startActivity(homeIntent);
238         try {
239             Thread.sleep(mWorkspaceLaunchTimeout);
240         } catch (InterruptedException e) {
241             // ignore
242         }
243     }
244 
getLaunchIntentForPackage(String packageName)245     private Intent getLaunchIntentForPackage(String packageName) {
246         UiModeManager umm = (UiModeManager) mContext.getSystemService(Context.UI_MODE_SERVICE);
247         boolean isLeanback = umm.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION;
248         Intent intent = null;
249         if (isLeanback) {
250             intent = mPackageManager.getLeanbackLaunchIntentForPackage(packageName);
251         } else {
252             intent = mPackageManager.getLaunchIntentForPackage(packageName);
253         }
254         return intent;
255     }
256 
257     /**
258      * Launches and activity and queries for errors.
259      *
260      * @param packageName {@link String} the package name of the application to
261      *            launch.
262      * @return {@link Collection} of {@link ProcessErrorStateInfo} detected
263      *         during the app launch.
264      */
launchActivity(String packageName, Intent intent)265     private void launchActivity(String packageName, Intent intent) {
266         Log.d(TAG, String.format("launching package \"%s\" with intent: %s",
267                 packageName, intent.toString()));
268 
269         // Launch Activity
270         mContext.startActivity(intent);
271 
272         try {
273             // artificial delay: in case app crashes after doing some work during launch
274             Thread.sleep(mAppLaunchTimeout);
275         } catch (InterruptedException e) {
276             // ignore
277         }
278     }
279 
addProcessError(String processName, String errorType, String errorInfo)280     private void addProcessError(String processName, String errorType, String errorInfo) {
281         // parse out the package name if necessary, for apps with multiple proceses
282         String pkgName = processName.split(":", 2)[0];
283         List<String> errors;
284         if (mAppErrors.containsKey(pkgName)) {
285             errors = mAppErrors.get(pkgName);
286         }  else {
287             errors = new ArrayList<>();
288         }
289         errors.add(String.format("### Type: %s, Details:\n%s", errorType, errorInfo));
290         mAppErrors.put(pkgName, errors);
291     }
292 
293     /**
294      * Determine if a given package is still running.
295      *
296      * @param packageName {@link String} package to look for
297      * @return True if package is running, false otherwise.
298      */
processStillUp(String packageName)299     private boolean processStillUp(String packageName) {
300         @SuppressWarnings("deprecation")
301         List<RunningTaskInfo> infos = mActivityManager.getRunningTasks(100);
302         for (RunningTaskInfo info : infos) {
303             if (info.baseActivity.getPackageName().equals(packageName)) {
304                 return true;
305             }
306         }
307         return false;
308     }
309 
310     /**
311      * An {@link IActivityController} that instructs framework to kill processes hitting crashes
312      * directly without showing crash dialogs
313      *
314      */
315     private class CrashSuppressor extends IActivityController.Stub {
316 
317         @Override
activityStarting(Intent intent, String pkg)318         public boolean activityStarting(Intent intent, String pkg) throws RemoteException {
319             Log.d(TAG, "activity starting: " + intent.getComponent().toShortString());
320             return true;
321         }
322 
323         @Override
activityResuming(String pkg)324         public boolean activityResuming(String pkg) throws RemoteException {
325             Log.d(TAG, "activity resuming: " + pkg);
326             return true;
327         }
328 
329         @Override
appCrashed(String processName, int pid, String shortMsg, String longMsg, long timeMillis, String stackTrace)330         public boolean appCrashed(String processName, int pid, String shortMsg, String longMsg,
331                 long timeMillis, String stackTrace) throws RemoteException {
332             Log.d(TAG, "app crash: " + processName);
333             addProcessError(processName, "crash", stackTrace);
334             // don't show dialog
335             return false;
336         }
337 
338         @Override
appEarlyNotResponding(String processName, int pid, String annotation)339         public int appEarlyNotResponding(String processName, int pid, String annotation)
340                 throws RemoteException {
341             // ignore
342             return 0;
343         }
344 
345         @Override
appNotResponding(String processName, int pid, String processStats)346         public int appNotResponding(String processName, int pid, String processStats)
347                 throws RemoteException {
348             Log.d(TAG, "app ANR: " + processName);
349             addProcessError(processName, "ANR", processStats);
350             // don't show dialog
351             return -1;
352         }
353 
354         @Override
systemNotResponding(String msg)355         public int systemNotResponding(String msg) throws RemoteException {
356             // ignore
357             return -1;
358         }
359     }
360 }
361