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