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