| /* |
| * Copyright (C) 2023 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.safetycenter.testing |
| |
| import android.content.Context |
| import android.os.Build.VERSION_CODES.TIRAMISU |
| import android.os.SystemClock |
| import android.safetycenter.SafetySourceData |
| import android.safetycenter.SafetySourceIssue |
| import android.safetycenter.config.SafetySourcesGroup |
| import android.util.Log |
| import androidx.annotation.RequiresApi |
| import androidx.test.uiautomator.By |
| import androidx.test.uiautomator.BySelector |
| import androidx.test.uiautomator.StaleObjectException |
| import androidx.test.uiautomator.UiDevice |
| import androidx.test.uiautomator.UiObject2 |
| import com.android.compatibility.common.util.SystemUtil.runShellCommand |
| import com.android.compatibility.common.util.UiAutomatorUtils2.getUiDevice |
| import com.android.compatibility.common.util.UiAutomatorUtils2.waitFindObject |
| import com.android.compatibility.common.util.UiAutomatorUtils2.waitFindObjectOrNull |
| import java.time.Duration |
| import java.util.concurrent.TimeoutException |
| import java.util.regex.Pattern |
| |
| /** A class that helps with UI testing. */ |
| object UiTestHelper { |
| |
| /** The label of the rescan button. */ |
| const val RESCAN_BUTTON_LABEL = "Scan device" |
| /** The title of collapsible card that controls the visibility of additional issue cards. */ |
| const val MORE_ISSUES_LABEL = "More alerts" |
| |
| private const val DISMISS_ISSUE_LABEL = "Dismiss" |
| private val WAIT_TIMEOUT = Duration.ofSeconds(25) |
| private val NOT_DISPLAYED_TIMEOUT = Duration.ofMillis(500) |
| |
| private val TAG = UiTestHelper::class.java.simpleName |
| |
| /** |
| * Waits for the given [selector] to be displayed and performs the given [uiObjectAction] on it. |
| */ |
| fun waitDisplayed(selector: BySelector, uiObjectAction: (UiObject2) -> Unit = {}) { |
| waitFor("$selector to be displayed", WAIT_TIMEOUT) { |
| uiObjectAction(waitFindObject(selector, it.toMillis())) |
| true |
| } |
| } |
| |
| /** Waits for all the given [textToFind] to be displayed. */ |
| fun waitAllTextDisplayed(vararg textToFind: CharSequence?) { |
| for (text in textToFind) { |
| if (text != null) waitDisplayed(By.text(text.toString())) |
| } |
| } |
| |
| /** |
| * Waits for a button with the given [label] to be displayed and performs the given |
| * [uiObjectAction] on it. |
| */ |
| fun waitButtonDisplayed(label: CharSequence, uiObjectAction: (UiObject2) -> Unit = {}) = |
| waitDisplayed(buttonSelector(label), uiObjectAction) |
| |
| /** Waits for the given [selector] not to be displayed. */ |
| fun waitNotDisplayed(selector: BySelector) { |
| waitFor("$selector not to be displayed", NOT_DISPLAYED_TIMEOUT) { |
| waitFindObjectOrNull(selector, it.toMillis()) == null |
| } |
| } |
| |
| /** Waits for all the given [textToFind] not to be displayed. */ |
| fun waitAllTextNotDisplayed(vararg textToFind: CharSequence?) { |
| for (text in textToFind) { |
| if (text != null) waitNotDisplayed(By.text(text.toString())) |
| } |
| } |
| |
| /** Waits for a button with the given [label] not to be displayed. */ |
| fun waitButtonNotDisplayed(label: CharSequence) { |
| waitNotDisplayed(buttonSelector(label)) |
| } |
| |
| /** |
| * Waits for most of the [SafetySourceData] information to be displayed. |
| * |
| * This includes its UI entry and its issues. |
| */ |
| @RequiresApi(TIRAMISU) |
| fun waitSourceDataDisplayed(sourceData: SafetySourceData) { |
| waitAllTextDisplayed(sourceData.status?.title, sourceData.status?.summary) |
| |
| for (sourceIssue in sourceData.issues) { |
| waitSourceIssueDisplayed(sourceIssue) |
| } |
| } |
| |
| /** Waits for most of the [SafetySourceIssue] information to be displayed. */ |
| @RequiresApi(TIRAMISU) |
| fun waitSourceIssueDisplayed(sourceIssue: SafetySourceIssue) { |
| waitAllTextDisplayed(sourceIssue.title, sourceIssue.subtitle, sourceIssue.summary) |
| |
| for (action in sourceIssue.actions) { |
| waitButtonDisplayed(action.label) |
| } |
| } |
| |
| /** Waits for most of the [SafetySourceIssue] information not to be displayed. */ |
| @RequiresApi(TIRAMISU) |
| fun waitSourceIssueNotDisplayed(sourceIssue: SafetySourceIssue) { |
| waitAllTextNotDisplayed(sourceIssue.title) |
| } |
| |
| /** Waits for the specified screen title to be displayed. */ |
| fun waitPageTitleDisplayed(title: String) { |
| waitDisplayed(By.desc(title)) |
| } |
| |
| /** Waits for the specified screen title not to be displayed. */ |
| fun waitPageTitleNotDisplayed(title: String) { |
| waitNotDisplayed(By.desc(title)) |
| } |
| |
| /** Waits for the group title and summary to be displayed on the homepage */ |
| fun waitGroupShownOnHomepage(context: Context, group: SafetySourcesGroup) { |
| waitAllTextDisplayed( |
| context.getString(group.titleResId), |
| context.getString(group.summaryResId) |
| ) |
| } |
| |
| /** Dismisses the issue card by clicking the dismiss button. */ |
| fun clickDismissIssueCard() { |
| waitDisplayed(By.desc(DISMISS_ISSUE_LABEL)) { it.click() } |
| } |
| |
| /** Confirms the dismiss action by clicking on the dialog that pops up. */ |
| fun clickConfirmDismissal() { |
| waitButtonDisplayed(DISMISS_ISSUE_LABEL) { it.click() } |
| } |
| |
| /** Clicks the brand chip button on a subpage in Safety Center. */ |
| fun clickSubpageBrandChip() { |
| waitButtonDisplayed("Security & privacy") { it.click() } |
| } |
| |
| /** Opens the subpage by clicking on the group title. */ |
| fun clickOpenSubpage(context: Context, group: SafetySourcesGroup) { |
| waitDisplayed(By.text(context.getString(group.titleResId))) { it.click() } |
| } |
| |
| /** Clicks the more issues card button to show or hide additional issues. */ |
| fun clickMoreIssuesCard() { |
| waitDisplayed(By.text(MORE_ISSUES_LABEL)) { it.click() } |
| } |
| |
| /** Enables or disables animations based on [enabled]. */ |
| fun setAnimationsEnabled(enabled: Boolean) { |
| val scale = |
| if (enabled) { |
| "1" |
| } else { |
| "0" |
| } |
| runShellCommand("settings put global window_animation_scale $scale") |
| runShellCommand("settings put global transition_animation_scale $scale") |
| runShellCommand("settings put global animator_duration_scale $scale") |
| } |
| |
| fun UiDevice.rotate() { |
| unfreezeRotation() |
| if (isNaturalOrientation) { |
| setOrientationLeft() |
| } else { |
| setOrientationNatural() |
| } |
| freezeRotation() |
| waitForIdle() |
| } |
| |
| fun UiDevice.resetRotation() { |
| if (!isNaturalOrientation) { |
| unfreezeRotation() |
| setOrientationNatural() |
| freezeRotation() |
| waitForIdle() |
| } |
| } |
| |
| private fun buttonSelector(label: CharSequence): BySelector { |
| return By.clickable(true).text(Pattern.compile("$label|${label.toString().uppercase()}")) |
| } |
| |
| private fun waitFor( |
| message: String, |
| uiAutomatorConditionTimeout: Duration, |
| uiAutomatorCondition: (Duration) -> Boolean |
| ) { |
| val elapsedStartMillis = SystemClock.elapsedRealtime() |
| while (true) { |
| getUiDevice().waitForIdle() |
| val durationSinceStart = |
| Duration.ofMillis(SystemClock.elapsedRealtime() - elapsedStartMillis) |
| if (durationSinceStart >= WAIT_TIMEOUT) { |
| break |
| } |
| val remainingTime = WAIT_TIMEOUT - durationSinceStart |
| val uiAutomatorTimeout = minOf(uiAutomatorConditionTimeout, remainingTime) |
| try { |
| if (uiAutomatorCondition(uiAutomatorTimeout)) { |
| return |
| } else { |
| Log.d(TAG, "Failed condition for $message, will retry if within timeout") |
| } |
| } catch (e: StaleObjectException) { |
| Log.d(TAG, "StaleObjectException for $message, will retry if within timeout", e) |
| } |
| } |
| |
| throw TimeoutException("Timed out waiting for $message") |
| } |
| } |