Home | History | Annotate | Download | only in compatibilitytest
      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