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