blob: 50ec0de4a520ec122bac138e0c2e0d9da7adb88c [file] [log] [blame]
/*
* Copyright (C) 2016 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.afwtest.uiautomator.test;
import static com.android.afwtest.common.Constants.KEY_MUTE_APP_CRASH_DIALOGS;
import static com.android.afwtest.uiautomator.Constants.ANDROID_PKG_NAME;
import static com.android.afwtest.uiautomator.Constants.MANAGED_PROVISIONING_PKG_NAME;
import static com.android.afwtest.uiautomator.utils.WidgetUtils.safeClick;
import static com.android.afwtest.uiautomator.utils.WidgetUtils.safeClickAny;
import android.support.test.uiautomator.By;
import android.support.test.uiautomator.BySelector;
import android.support.test.uiautomator.UiDevice;
import android.support.test.uiautomator.UiObject2;
import android.support.test.uiautomator.UiWatcher;
import android.text.TextUtils;
import android.util.Log;
import android.widget.Button;
import com.android.afwtest.common.test.TestConfig;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Pattern;
/**
* {@link UiWatcher} to handle common unexpected scenarios.
*
* <p>
* This is a singleton class. There can be only one
* </p>
*/
public final class AfwTestUiWatcher implements UiWatcher {
private static final String TAG = "afwtest.AfwTestUiWatcher";
/**
* Unique name of this ui watcher.
*/
private static final String UI_WATCHER_NAME = "afw-test-uiwatcher";
/**
* Regex string for app crashed message.
*/
private static final String APP_STOPPED_MSG_REGEX = "Unfortunately,.*has stopped.*";
/**
* {@link Pattern} to match app crashed message.
*/
private static final Pattern APP_STOPPED_MSG_PATTERN = Pattern.compile(APP_STOPPED_MSG_REGEX);
/**
* {@link BySelector} for app crashed message.
*/
private static final BySelector APP_STOPPED_MSG_SELECTOR =
By.res(ANDROID_PKG_NAME, "message")
.text(APP_STOPPED_MSG_PATTERN);
/**
* {@link BySelector} for "OK" button on app crash dialog.
*/
private static final BySelector APP_STOPPED_DIALOG_OK_BUTTON_SELECTOR =
By.clazz(Button.class.getName())
.text("OK");
/**
* Words to match for "Accept" button's text or description.
*/
private static final String[] ACCEPT_WORDS = {
"[aA]ccept", "ACCEPT",
"[aA]gree", "AGREE",
"[aA]llow", "ALLOW",
"[cC]lose", "CLOSE"};
/**
* {@link Pattern} to match any "Accept" word in {@link #ACCEPT_WORDS}.
*/
private static final Pattern ACCEPT_BTN_PATTERN =
Pattern.compile(TextUtils.join("|", ACCEPT_WORDS));
/**
* Buttons with text matching {@link #ACCEPT_BTN_PATTERN}.
*/
private static final BySelector ACCEPT_BTN_TEXT_SELECTOR =
By.enabled(true)
.checkable(false)
.clickable(true)
.text(ACCEPT_BTN_PATTERN);
/**
* Buttons with content description matching {@link #ACCEPT_BTN_PATTERN}.
*/
private static final BySelector ACCEPT_BTN_DESC_SELECTOR =
By.enabled(true)
.checkable(false)
.clickable(true)
.desc(ACCEPT_BTN_PATTERN);
/**
* Non-null string indicates fatal app crashed.
*/
private static String sFatalAppCrashMsg;
/**
* Assert if any of these app crashes.
*/
private static final List<String> FATAL_APP_CRASHES =
Arrays.asList(MANAGED_PROVISIONING_PKG_NAME,
"Setup Wizard");
/**
* List of packages whose crash should be ignored.
*/
private final List<String> mAppCrashWhitelist;
/**
* {@link UiDevice} object.
*/
private final UiDevice mUiDevice;
/**
* Constructor.
*
* @param uiDevice {@link UiDevice} object
*/
private AfwTestUiWatcher(UiDevice uiDevice) throws Exception {
mUiDevice = uiDevice;
mAppCrashWhitelist = new ArrayList<String>();
mAppCrashWhitelist.addAll(TestConfig.getDefault().getAppCrashWhitelist());
}
/**
* Registers this ui watcher.
*
* @param uiDevice {@link UiDevice} object
*/
public static void register(UiDevice uiDevice) throws Exception {
uiDevice.registerWatcher(UI_WATCHER_NAME, new AfwTestUiWatcher(uiDevice));
}
/**
* Unregisters this ui watcher.
*
* @param uiDevice {@link UiDevice} object
*/
public static void unregister(UiDevice uiDevice) {
uiDevice.removeWatcher(UI_WATCHER_NAME);
}
public static String getFatalAppCrashMsg() {
return sFatalAppCrashMsg;
}
/**
* {@inheritDoc}
*/
@Override
public boolean checkForCondition() {
// The order of the conditions matters.
try {
return clearAppStoppedDialog() || checkAcceptButtons();
} catch (Exception e) {
// Don't throw exception as it will crashes the test runner
Log.e(TAG, UI_WATCHER_NAME, e);
return false;
}
}
/**
* Clears dialogs: "Unfortunately, {app name} has stopped."
*
* @return {@code true} if any dialog is dismissed, {@code false} otherwise
*/
private boolean clearAppStoppedDialog() throws IOException {
if (mUiDevice.hasObject(APP_STOPPED_MSG_SELECTOR)
&& mUiDevice.hasObject(APP_STOPPED_DIALOG_OK_BUTTON_SELECTOR)) {
UiObject2 msgWidget = mUiDevice.findObject(APP_STOPPED_MSG_SELECTOR);
if (msgWidget != null) {
String msg = msgWidget.getText();
Log.w(TAG, String.format("Found app crash dialog: %s", msg));
if (isFatalAppCrash(msg)) {
Log.w(TAG, String.format("Fatal app crash. Test should abort.", msg));
sFatalAppCrashMsg = msg;
return true;
}
Log.w(TAG, String.format("Auto closing: %s", msg));
sFatalAppCrashMsg = null;
return safeClick(mUiDevice.findObject(APP_STOPPED_DIALOG_OK_BUTTON_SELECTOR));
}
}
return false;
}
/**
* Checks if app crash is fatal.
*
* @param appCrashMsg App crash message
* @return {@code true} if app crash is fatal, {@code false} otherwise.
*/
private boolean isFatalAppCrash(String appCrashMsg) throws IOException {
for (String app : FATAL_APP_CRASHES) {
if (appCrashMsg.contains(app)) {
return true;
}
}
// if muting all app crash dialog is enabled, return
if (TestConfig.getDefault().muteAppCrashDialogs()) {
Log.i(TAG, String.format("%s=true", KEY_MUTE_APP_CRASH_DIALOGS));
return false;
}
// otherwise, auto close whitelisted app crashes, assert on others.
for (String app : mAppCrashWhitelist) {
if (appCrashMsg.contains(app)) {
Log.w(TAG, String.format("Whitelisted app crash: %s", app));
return false;
}
}
return true;
}
/**
* Clicks any visible accept button.
*
* @return {@code true} if any button found and clicked successfully;
* {@code false} otherwise
*/
private boolean checkAcceptButtons() {
return safeClickAny(mUiDevice.findObjects(ACCEPT_BTN_TEXT_SELECTOR)) ||
safeClickAny(mUiDevice.findObjects(ACCEPT_BTN_DESC_SELECTOR));
}
}