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