/* * Copyright (C) 2012 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.compatibilitytest; import android.app.ActivityManager; import android.app.ActivityManager.ProcessErrorStateInfo; import android.app.ActivityManager.RunningTaskInfo; import android.app.IActivityController; import android.app.IActivityManager; import android.app.Instrumentation; import android.app.UiAutomation; import android.app.UiModeManager; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.res.Configuration; import android.os.Bundle; import android.os.DropBoxManager; import android.os.RemoteException; import android.os.ServiceManager; import android.util.Log; import androidx.test.InstrumentationRegistry; import androidx.test.runner.AndroidJUnit4; import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; /** * Application Compatibility Test that launches an application and detects * crashes. */ @RunWith(AndroidJUnit4.class) public class AppCompatibility { private static final String TAG = AppCompatibility.class.getSimpleName(); private static final String PACKAGE_TO_LAUNCH = "package_to_launch"; private static final String APP_LAUNCH_TIMEOUT_MSECS = "app_launch_timeout_ms"; private static final String WORKSPACE_LAUNCH_TIMEOUT_MSECS = "workspace_launch_timeout_ms"; private static final Set<String> DROPBOX_TAGS = new HashSet<>(); static { DROPBOX_TAGS.add("SYSTEM_TOMBSTONE"); DROPBOX_TAGS.add("system_app_anr"); DROPBOX_TAGS.add("system_app_native_crash"); DROPBOX_TAGS.add("system_app_crash"); DROPBOX_TAGS.add("data_app_anr"); DROPBOX_TAGS.add("data_app_native_crash"); DROPBOX_TAGS.add("data_app_crash"); } private static final int MAX_CRASH_SNIPPET_LINES = 20; private static final int MAX_NUM_CRASH_SNIPPET = 3; // time waiting for app to launch private int mAppLaunchTimeout = 7000; // time waiting for launcher home screen to show up private int mWorkspaceLaunchTimeout = 2000; private Context mContext; private ActivityManager mActivityManager; private PackageManager mPackageManager; private Bundle mArgs; private Instrumentation mInstrumentation; private String mLauncherPackageName; private IActivityController mCrashSupressor = new CrashSuppressor(); private Map<String, List<String>> mAppErrors = new HashMap<>(); @Before public void setUp() throws Exception { mInstrumentation = InstrumentationRegistry.getInstrumentation(); mContext = InstrumentationRegistry.getTargetContext(); mActivityManager = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE); mPackageManager = mContext.getPackageManager(); mArgs = InstrumentationRegistry.getArguments(); // resolve launcher package name Intent intent = new Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME); ResolveInfo resolveInfo = mPackageManager.resolveActivity( intent, PackageManager.MATCH_DEFAULT_ONLY); mLauncherPackageName = resolveInfo.activityInfo.packageName; Assert.assertNotNull("failed to resolve package name for launcher", mLauncherPackageName); Log.v(TAG, "Using launcher package name: " + mLauncherPackageName); // Parse optional inputs. String appLaunchTimeoutMsecs = mArgs.getString(APP_LAUNCH_TIMEOUT_MSECS); if (appLaunchTimeoutMsecs != null) { mAppLaunchTimeout = Integer.parseInt(appLaunchTimeoutMsecs); } String workspaceLaunchTimeoutMsecs = mArgs.getString(WORKSPACE_LAUNCH_TIMEOUT_MSECS); if (workspaceLaunchTimeoutMsecs != null) { mWorkspaceLaunchTimeout = Integer.parseInt(workspaceLaunchTimeoutMsecs); } mInstrumentation.getUiAutomation().setRotation(UiAutomation.ROTATION_FREEZE_0); // set activity controller to suppress crash dialogs and collects them by process name mAppErrors.clear(); IActivityManager.Stub.asInterface(ServiceManager.checkService(Context.ACTIVITY_SERVICE)) .setActivityController(mCrashSupressor, false); } @After public void tearDown() throws Exception { // unset activity controller IActivityManager.Stub.asInterface(ServiceManager.checkService(Context.ACTIVITY_SERVICE)) .setActivityController(null, false); mInstrumentation.getUiAutomation().setRotation(UiAutomation.ROTATION_UNFREEZE); } /** * Actual test case that launches the package and throws an exception on the * first error. * * @throws Exception */ @Test public void testAppStability() throws Exception { String packageName = mArgs.getString(PACKAGE_TO_LAUNCH); if (packageName != null) { Log.d(TAG, "Launching app " + packageName); Intent intent = getLaunchIntentForPackage(packageName); if (intent == null) { Log.w(TAG, String.format("Skipping %s; no launch intent", packageName)); return; } long startTime = System.currentTimeMillis(); launchActivity(packageName, intent); try { checkDropbox(startTime, packageName); if (mAppErrors.containsKey(packageName)) { StringBuilder message = new StringBuilder("Error(s) detected for package: ") .append(packageName); List<String> errors = mAppErrors.get(packageName); for (int i = 0; i < MAX_NUM_CRASH_SNIPPET && i < errors.size(); i++) { String err = errors.get(i); message.append("\n\n"); // limit the size of each crash snippet message.append(truncate(err, MAX_CRASH_SNIPPET_LINES)); } if (errors.size() > MAX_NUM_CRASH_SNIPPET) { message.append(String.format("\n... %d more errors omitted ...", errors.size() - MAX_NUM_CRASH_SNIPPET)); } Assert.fail(message.toString()); } // last check: see if app process is still running Assert.assertTrue("app package \"" + packageName + "\" no longer found in running " + "tasks, but no explicit crashes were detected; check logcat for details", processStillUp(packageName)); } finally { returnHome(); } } else { Log.d(TAG, "Missing argument, use " + PACKAGE_TO_LAUNCH + " to specify the package to launch"); } } /** * Truncate the text to at most the specified number of lines, and append a marker at the end * when truncated * @param text * @param maxLines * @return */ private static String truncate(String text, int maxLines) { String[] lines = text.split("\\r?\\n"); StringBuilder ret = new StringBuilder(); for (int i = 0; i < maxLines && i < lines.length; i++) { ret.append(lines[i]); ret.append('\n'); } if (lines.length > maxLines) { ret.append("... "); ret.append(lines.length - maxLines); ret.append(" more lines truncated ...\n"); } return ret.toString(); } /** * Check dropbox for entries of interest regarding the specified process * @param startTime if not 0, only check entries with timestamp later than the start time * @param processName the process name to check for */ private void checkDropbox(long startTime, String processName) { DropBoxManager dropbox = (DropBoxManager) mContext .getSystemService(Context.DROPBOX_SERVICE); DropBoxManager.Entry entry = null; while (null != (entry = dropbox.getNextEntry(null, startTime))) { try { // only check entries with tag that's of interest String tag = entry.getTag(); if (DROPBOX_TAGS.contains(tag)) { String content = entry.getText(4096); if (content != null) { if (content.contains(processName)) { addProcessError(processName, "dropbox:" + tag, content); } } } startTime = entry.getTimeMillis(); } finally { entry.close(); } } } private void returnHome() { Intent homeIntent = new Intent(Intent.ACTION_MAIN); homeIntent.addCategory(Intent.CATEGORY_HOME); homeIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); // Send the "home" intent and wait 2 seconds for us to get there mContext.startActivity(homeIntent); try { Thread.sleep(mWorkspaceLaunchTimeout); } catch (InterruptedException e) { // ignore } } private Intent getLaunchIntentForPackage(String packageName) { UiModeManager umm = (UiModeManager) mContext.getSystemService(Context.UI_MODE_SERVICE); boolean isLeanback = umm.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION; Intent intent = null; if (isLeanback) { intent = mPackageManager.getLeanbackLaunchIntentForPackage(packageName); } else { intent = mPackageManager.getLaunchIntentForPackage(packageName); } return intent; } /** * Launches and activity and queries for errors. * * @param packageName {@link String} the package name of the application to * launch. * @return {@link Collection} of {@link ProcessErrorStateInfo} detected * during the app launch. */ private void launchActivity(String packageName, Intent intent) { Log.d(TAG, String.format("launching package \"%s\" with intent: %s", packageName, intent.toString())); // Launch Activity mContext.startActivity(intent); try { // artificial delay: in case app crashes after doing some work during launch Thread.sleep(mAppLaunchTimeout); } catch (InterruptedException e) { // ignore } } private void addProcessError(String processName, String errorType, String errorInfo) { // parse out the package name if necessary, for apps with multiple proceses String pkgName = processName.split(":", 2)[0]; List<String> errors; if (mAppErrors.containsKey(pkgName)) { errors = mAppErrors.get(pkgName); } else { errors = new ArrayList<>(); } errors.add(String.format("### Type: %s, Details:\n%s", errorType, errorInfo)); mAppErrors.put(pkgName, errors); } /** * Determine if a given package is still running. * * @param packageName {@link String} package to look for * @return True if package is running, false otherwise. */ private boolean processStillUp(String packageName) { @SuppressWarnings("deprecation") List<RunningTaskInfo> infos = mActivityManager.getRunningTasks(100); for (RunningTaskInfo info : infos) { if (info.baseActivity.getPackageName().equals(packageName)) { return true; } } return false; } /** * An {@link IActivityController} that instructs framework to kill processes hitting crashes * directly without showing crash dialogs * */ private class CrashSuppressor extends IActivityController.Stub { @Override public boolean activityStarting(Intent intent, String pkg) throws RemoteException { Log.d(TAG, "activity starting: " + intent.getComponent().toShortString()); return true; } @Override public boolean activityResuming(String pkg) throws RemoteException { Log.d(TAG, "activity resuming: " + pkg); return true; } @Override public boolean appCrashed(String processName, int pid, String shortMsg, String longMsg, long timeMillis, String stackTrace) throws RemoteException { Log.d(TAG, "app crash: " + processName); addProcessError(processName, "crash", stackTrace); // don't show dialog return false; } @Override public int appEarlyNotResponding(String processName, int pid, String annotation) throws RemoteException { // ignore return 0; } @Override public int appNotResponding(String processName, int pid, String processStats) throws RemoteException { Log.d(TAG, "app ANR: " + processName); addProcessError(processName, "ANR", processStats); // don't show dialog return -1; } @Override public int systemNotResponding(String msg) throws RemoteException { // ignore return -1; } } }