1 /*
2  * Copyright (C) 2016 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 android.test.appsmoke;
18 
19 import android.app.ActivityManagerNative;
20 import android.app.IActivityController;
21 import android.app.Instrumentation;
22 import android.content.Context;
23 import android.content.Intent;
24 import android.content.pm.LauncherActivityInfo;
25 import android.content.pm.LauncherApps;
26 import android.os.Bundle;
27 import android.os.RemoteException;
28 import android.os.SystemClock;
29 import android.os.UserHandle;
30 import android.os.UserManager;
31 import android.support.test.InstrumentationRegistry;
32 import android.support.test.launcherhelper.ILauncherStrategy;
33 import android.support.test.launcherhelper.LauncherStrategyFactory;
34 import android.support.test.uiautomator.UiDevice;
35 import android.util.Log;
36 
37 import org.junit.After;
38 import org.junit.AfterClass;
39 import org.junit.Assert;
40 import org.junit.Before;
41 import org.junit.BeforeClass;
42 import org.junit.Test;
43 import org.junit.runner.RunWith;
44 import org.junit.runners.Parameterized;
45 import org.junit.runners.Parameterized.Parameter;
46 import org.junit.runners.Parameterized.Parameters;
47 
48 import java.util.ArrayList;
49 import java.util.Arrays;
50 import java.util.Collection;
51 import java.util.Collections;
52 import java.util.HashSet;
53 import java.util.List;
54 import java.util.Set;
55 
56 @RunWith(Parameterized.class)
57 public class AppSmokeTest {
58 
59     private static final String TAG = AppSmokeTest.class.getSimpleName();
60     private static final String EXCLUDE_LIST = "exclude_apps";
61     private static final String DEBUG_LIST = "debug_apps";
62     private static final long WAIT_FOR_ANR = 6000;
63 
64     @Parameter
65     public LaunchParameter mAppInfo;
66 
67     private boolean mAppHasError = false;
68     private boolean mLaunchIntentDetected = false;
69     private ILauncherStrategy mLauncherStrategy = null;
70     private static UiDevice sDevice = null;
71 
72     /**
73      * Convenient internal class to hold some launch specific data
74      */
75     private static class LaunchParameter implements Comparable<LaunchParameter>{
76         public String appName;
77         public String packageName;
78         public String activityName;
79 
LaunchParameter(String appName, String packageName, String activityName)80         private LaunchParameter(String appName, String packageName, String activityName) {
81             this.appName = appName;
82             this.packageName = packageName;
83             this.activityName = activityName;
84         }
85 
86         @Override
compareTo(LaunchParameter another)87         public int compareTo(LaunchParameter another) {
88             return appName.compareTo(another.appName);
89         }
90 
91         @Override
toString()92         public String toString() {
93             return appName;
94         }
95 
toLongString()96         public String toLongString() {
97             return String.format("%s [activity: %s/%s]", appName, packageName, activityName);
98         }
99     }
100 
101     /**
102      * an activity controller to detect app launch crashes/ANR etc
103      */
104     private IActivityController mActivityController = new IActivityController.Stub() {
105 
106         @Override
107         public int systemNotResponding(String msg) throws RemoteException {
108             // let system die
109             return -1;
110         }
111 
112         @Override
113         public int appNotResponding(String processName, int pid, String processStats)
114                 throws RemoteException {
115             if (processName.startsWith(mAppInfo.packageName)) {
116                 mAppHasError = true;
117             }
118             // kill app
119             return -1;
120         }
121 
122         @Override
123         public int appEarlyNotResponding(String processName, int pid, String annotation)
124                 throws RemoteException {
125             // do nothing
126             return 0;
127         }
128 
129         @Override
130         public boolean appCrashed(String processName, int pid, String shortMsg, String longMsg,
131                 long timeMillis, String stackTrace) throws RemoteException {
132             if (processName.startsWith(mAppInfo.packageName)) {
133                 mAppHasError = true;
134             }
135             return false;
136         }
137 
138         @Override
139         public boolean activityStarting(Intent intent, String pkg) throws RemoteException {
140             Log.d(TAG, String.format("activityStarting: pkg=%s intent=%s",
141                     pkg, intent.toInsecureString()));
142             // always allow starting
143             if (pkg.equals(mAppInfo.packageName)) {
144                 mLaunchIntentDetected = true;
145             }
146             return true;
147         }
148 
149         @Override
150         public boolean activityResuming(String pkg) throws RemoteException {
151             Log.d(TAG, String.format("activityResuming: pkg=%s", pkg));
152             // always allow resuming
153             return true;
154         }
155     };
156 
157     /**
158      * Generate the list of apps to test for launches by querying package manager
159      * @return
160      */
161     @Parameters(name = "{0}")
generateAppsList()162     public static Collection<LaunchParameter> generateAppsList() {
163         Instrumentation instr = InstrumentationRegistry.getInstrumentation();
164         Bundle args = InstrumentationRegistry.getArguments();
165         Context ctx = instr.getTargetContext();
166         List<LaunchParameter> ret = new ArrayList<>();
167         Set<String> excludedApps = new HashSet<>();
168         Set<String> debugApps = new HashSet<>();
169 
170         // parse list of app names that should be execluded from launch tests
171         if (args.containsKey(EXCLUDE_LIST)) {
172             excludedApps.addAll(Arrays.asList(args.getString(EXCLUDE_LIST).split(",")));
173         }
174         // parse list of app names used for debugging (i.e. essentially a whitelist)
175         if (args.containsKey(DEBUG_LIST)) {
176             debugApps.addAll(Arrays.asList(args.getString(DEBUG_LIST).split(",")));
177         }
178         LauncherApps la = (LauncherApps)ctx.getSystemService(Context.LAUNCHER_APPS_SERVICE);
179         UserManager um = (UserManager)ctx.getSystemService(Context.USER_SERVICE);
180         List<LauncherActivityInfo> activities = new ArrayList<>();
181         for (UserHandle handle : um.getUserProfiles()) {
182             activities.addAll(la.getActivityList(null, handle));
183         }
184         for (LauncherActivityInfo info : activities) {
185             String label = info.getLabel().toString();
186             if (!debugApps.isEmpty()) {
187                 if (!debugApps.contains(label)) {
188                     // if debug apps non-empty, we are essentially in whitelist mode
189                     // bypass any apps not on list
190                     continue;
191                 }
192             } else if (excludedApps.contains(label)) {
193                 // if not debugging apps, bypass any excluded apps
194                 continue;
195             }
196             ret.add(new LaunchParameter(label, info
197                     .getApplicationInfo().packageName, info.getName()));
198         }
199         Collections.sort(ret);
200         return ret;
201     }
202 
203     @Before
before()204     public void before() throws RemoteException {
205         ActivityManagerNative.getDefault().setActivityController(mActivityController, false);
206         mLauncherStrategy = LauncherStrategyFactory.getInstance(sDevice).getLauncherStrategy();
207         mAppHasError = false;
208         mLaunchIntentDetected = false;
209     }
210 
211     @BeforeClass
beforeClass()212     public static void beforeClass() throws RemoteException {
213         sDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
214         sDevice.setOrientationNatural();
215     }
216 
217     @After
after()218     public void after() throws RemoteException {
219         sDevice.pressHome();
220         ActivityManagerNative.getDefault().forceStopPackage(
221                 mAppInfo.packageName, UserHandle.USER_ALL);
222         ActivityManagerNative.getDefault().setActivityController(null, false);
223     }
224 
225     @AfterClass
afterClass()226     public static void afterClass() throws RemoteException {
227         sDevice.unfreezeRotation();
228     }
229 
230     @Test
testAppLaunch()231     public void testAppLaunch() {
232         Log.d(TAG, "Launching: " + mAppInfo.toLongString());
233         long timestamp = mLauncherStrategy.launch(mAppInfo.appName, mAppInfo.packageName);
234         boolean launchResult = (timestamp != ILauncherStrategy.LAUNCH_FAILED_TIMESTAMP);
235         if (launchResult) {
236             // poke app to check if it's responsive
237             pokeApp();
238             SystemClock.sleep(WAIT_FOR_ANR);
239         }
240         if (mAppHasError) {
241             Assert.fail("app crash or ANR detected");
242         }
243         if (!launchResult && !mLaunchIntentDetected) {
244             Assert.fail("no app crash or ANR detected, but failed to launch via UI");
245         }
246         // if launchResult is false but mLaunchIntentDetected is true, we consider it as success
247         // this happens when an app is a trampoline activity to something else
248     }
249 
pokeApp()250     private void pokeApp() {
251         int w = sDevice.getDisplayWidth();
252         int h = sDevice.getDisplayHeight();
253         int dY = h / 4;
254         boolean ret = sDevice.swipe(w / 2, h / 2 + dY, w / 2, h / 2 - dY, 40);
255         if (!ret) {
256             Log.w(TAG, "Failed while attempting to poke front end window with swipe");
257         }
258     }
259 }
260