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