| /* |
| * 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 android.tools.common.flicker.subject.wm |
| |
| import android.tools.common.Rotation |
| import android.tools.common.datatypes.Region |
| import android.tools.common.datatypes.component.ComponentNameMatcher |
| import android.tools.common.datatypes.component.IComponentMatcher |
| import android.tools.common.flicker.assertions.Fact |
| import android.tools.common.flicker.subject.FlickerSubject |
| import android.tools.common.flicker.subject.region.RegionSubject |
| import android.tools.common.traces.wm.WindowManagerState |
| import android.tools.common.traces.wm.WindowState |
| |
| /** |
| * Subject for [WindowManagerState] objects, used to make assertions over behaviors that occur on a |
| * single WM state. |
| * |
| * To make assertions over a specific state from a trace it is recommended to create a subject using |
| * [WindowManagerTraceSubject](myTrace) and select the specific state using: |
| * ``` |
| * [WindowManagerTraceSubject.first] |
| * [WindowManagerTraceSubject.last] |
| * [WindowManagerTraceSubject.entry] |
| * ``` |
| * Alternatively, it is also possible to use [WindowManagerStateSubject](myState). |
| * |
| * Example: |
| * ``` |
| * val trace = WindowManagerTraceParser().parse(myTraceFile) |
| * val subject = WindowManagerTraceSubject(trace).first() |
| * .contains("ValidWindow") |
| * .notContains("ImaginaryWindow") |
| * .showsAboveAppWindow("NavigationBar") |
| * .invoke { myCustomAssertion(this) } |
| * ``` |
| */ |
| class WindowManagerStateSubject( |
| val wmState: WindowManagerState, |
| val trace: WindowManagerTraceSubject? = null, |
| override val parent: FlickerSubject? = null |
| ) : FlickerSubject(), IWindowManagerSubject<WindowManagerStateSubject, RegionSubject> { |
| override val timestamp = wmState.timestamp |
| override val selfFacts = listOf(Fact("WM State", wmState)) |
| |
| val subjects by lazy { wmState.windowStates.map { WindowStateSubject(this, timestamp, it) } } |
| |
| val appWindows: List<WindowStateSubject> |
| get() = subjects.filter { wmState.appWindows.contains(it.windowState) } |
| |
| val nonAppWindows: List<WindowStateSubject> |
| get() = subjects.filter { wmState.nonAppWindows.contains(it.windowState) } |
| |
| val aboveAppWindows: List<WindowStateSubject> |
| get() = subjects.filter { wmState.aboveAppWindows.contains(it.windowState) } |
| |
| val belowAppWindows: List<WindowStateSubject> |
| get() = subjects.filter { wmState.belowAppWindows.contains(it.windowState) } |
| |
| val visibleWindows: List<WindowStateSubject> |
| get() = subjects.filter { wmState.visibleWindows.contains(it.windowState) } |
| |
| val visibleAppWindows: List<WindowStateSubject> |
| get() = subjects.filter { wmState.visibleAppWindows.contains(it.windowState) } |
| |
| /** Executes a custom [assertion] on the current subject */ |
| operator fun invoke(assertion: (WindowManagerState) -> Unit): WindowManagerStateSubject = |
| apply { |
| assertion(this.wmState) |
| } |
| |
| /** {@inheritDoc} */ |
| override fun isEmpty(): WindowManagerStateSubject = apply { |
| check { "WM state is empty" }.that(subjects.isEmpty()).isEqual(true) |
| } |
| |
| /** {@inheritDoc} */ |
| override fun isNotEmpty(): WindowManagerStateSubject = apply { |
| check { "WM state is not empty" }.that(subjects.isEmpty()).isEqual(false) |
| } |
| |
| /** {@inheritDoc} */ |
| override fun visibleRegion(componentMatcher: IComponentMatcher?): RegionSubject { |
| val selectedWindows = |
| if (componentMatcher == null) { |
| // No filters so use all subjects |
| subjects |
| } else { |
| subjects.filter { componentMatcher.windowMatchesAnyOf(it.windowState) } |
| } |
| |
| if (selectedWindows.isEmpty()) { |
| val str = componentMatcher?.toWindowIdentifier() ?: "<any>" |
| fail(Fact(ASSERTION_TAG, "visibleRegion($str)"), Fact("Could not find windows", str)) |
| } |
| |
| val visibleWindows = selectedWindows.filter { it.isVisible } |
| val visibleRegions = visibleWindows.map { it.windowState.frameRegion }.toTypedArray() |
| return RegionSubject(visibleRegions, this, timestamp) |
| } |
| |
| /** {@inheritDoc} */ |
| override fun containsAboveAppWindow( |
| componentMatcher: IComponentMatcher |
| ): WindowManagerStateSubject = apply { contains(aboveAppWindows, componentMatcher) } |
| |
| /** {@inheritDoc} */ |
| override fun containsBelowAppWindow( |
| componentMatcher: IComponentMatcher |
| ): WindowManagerStateSubject = apply { contains(belowAppWindows, componentMatcher) } |
| |
| /** {@inheritDoc} */ |
| override fun isAboveWindow( |
| aboveWindowComponentMatcher: IComponentMatcher, |
| belowWindowComponentMatcher: IComponentMatcher |
| ): WindowManagerStateSubject = apply { |
| contains(aboveWindowComponentMatcher) |
| contains(belowWindowComponentMatcher) |
| |
| val aboveWindow = |
| wmState.windowStates.first { aboveWindowComponentMatcher.windowMatchesAnyOf(it) } |
| val belowWindow = |
| wmState.windowStates.first { belowWindowComponentMatcher.windowMatchesAnyOf(it) } |
| if (aboveWindow == belowWindow) { |
| fail( |
| Fact( |
| ASSERTION_TAG, |
| "Above and below windows should be different. " + |
| "Instead they were ${aboveWindow.title} " + |
| "(matched with ${aboveWindowComponentMatcher.toWindowIdentifier()} and " + |
| "${belowWindowComponentMatcher.toWindowIdentifier()})" |
| ) |
| ) |
| } |
| |
| // windows are ordered by z-order, from top to bottom |
| val aboveZ = |
| wmState.windowStates.indexOfFirst { aboveWindowComponentMatcher.windowMatchesAnyOf(it) } |
| val belowZ = |
| wmState.windowStates.indexOfFirst { belowWindowComponentMatcher.windowMatchesAnyOf(it) } |
| if (aboveZ >= belowZ) { |
| val matchedAboveWindow = |
| subjects.first { aboveWindowComponentMatcher.windowMatchesAnyOf(it.windowState) } |
| val aboveWindowTitleStr = aboveWindowComponentMatcher.toWindowIdentifier() |
| val belowWindowTitleStr = belowWindowComponentMatcher.toWindowIdentifier() |
| matchedAboveWindow.fail( |
| Fact( |
| ASSERTION_TAG, |
| "isAboveWindow(above=$aboveWindowTitleStr, below=$belowWindowTitleStr" |
| ), |
| Fact("Above", aboveWindowTitleStr), |
| Fact("Below", belowWindowTitleStr) |
| ) |
| } |
| } |
| |
| /** {@inheritDoc} */ |
| override fun containsNonAppWindow( |
| componentMatcher: IComponentMatcher |
| ): WindowManagerStateSubject = apply { contains(nonAppWindows, componentMatcher) } |
| |
| /** {@inheritDoc} */ |
| override fun isAppWindowOnTop(componentMatcher: IComponentMatcher): WindowManagerStateSubject = |
| apply { |
| if (wmState.visibleAppWindows.isEmpty()) { |
| fail( |
| Fact( |
| ASSERTION_TAG, |
| "isAppWindowOnTop(${componentMatcher.toWindowIdentifier()})" |
| ), |
| Fact("Not found", "No visible app windows found") |
| ) |
| } |
| val topVisibleAppWindow = wmState.topVisibleAppWindow |
| if ( |
| topVisibleAppWindow == null || |
| !componentMatcher.windowMatchesAnyOf(topVisibleAppWindow) |
| ) { |
| isNotEmpty() |
| val topWindow = subjects.first { it.windowState == topVisibleAppWindow } |
| topWindow.fail( |
| Fact( |
| ASSERTION_TAG, |
| "isAppWindowOnTop(${componentMatcher.toWindowIdentifier()})" |
| ), |
| Fact("Not on top", componentMatcher.toWindowIdentifier()), |
| Fact("Found", wmState.topVisibleAppWindow) |
| ) |
| } |
| } |
| |
| /** {@inheritDoc} */ |
| override fun isAppWindowNotOnTop( |
| componentMatcher: IComponentMatcher |
| ): WindowManagerStateSubject = apply { |
| val topVisibleAppWindow = wmState.topVisibleAppWindow |
| if ( |
| topVisibleAppWindow != null && componentMatcher.windowMatchesAnyOf(topVisibleAppWindow) |
| ) { |
| val topWindow = subjects.first { it.windowState == topVisibleAppWindow } |
| topWindow.fail( |
| Fact( |
| ASSERTION_TAG, |
| "isAppWindowNotOnTop(${componentMatcher.toWindowIdentifier()})" |
| ), |
| Fact("On top", componentMatcher.toWindowIdentifier()) |
| ) |
| } |
| } |
| |
| /** {@inheritDoc} */ |
| override fun doNotOverlap( |
| vararg componentMatcher: IComponentMatcher |
| ): WindowManagerStateSubject = apply { |
| check { |
| val repr = componentMatcher.joinToString(", ") { it.toWindowIdentifier() } |
| "Must give more than one window to check! (Given $repr)" |
| } |
| .that(componentMatcher.size) |
| .isEqual(1) |
| |
| componentMatcher.forEach { contains(it) } |
| val foundWindows = |
| componentMatcher |
| .toSet() |
| .associateWith { act -> |
| wmState.windowStates.firstOrNull { act.windowMatchesAnyOf(it) } |
| } |
| // keep entries only for windows that we actually found by removing nulls |
| .filterValues { it != null } |
| val foundWindowsRegions = |
| foundWindows.mapValues { (_, v) -> v?.frameRegion ?: Region.EMPTY } |
| |
| val regions = foundWindowsRegions.entries.toList() |
| for (i in regions.indices) { |
| val (ourTitle, ourRegion) = regions[i] |
| for (j in i + 1 until regions.size) { |
| val (otherTitle, otherRegion) = regions[j] |
| if (ourRegion.op(otherRegion, Region.Op.INTERSECT)) { |
| val window = foundWindows[ourTitle] ?: error("Window $ourTitle not found") |
| val windowSubject = subjects.first { it.windowState == window } |
| windowSubject.fail( |
| Fact( |
| ASSERTION_TAG, |
| "noWindowsOverlap" + |
| componentMatcher.joinToString { it.toWindowIdentifier() } |
| ), |
| Fact("Overlap", ourTitle), |
| Fact("Overlap", otherTitle) |
| ) |
| } |
| } |
| } |
| } |
| |
| /** {@inheritDoc} */ |
| override fun containsAppWindow(componentMatcher: IComponentMatcher): WindowManagerStateSubject = |
| apply { |
| // Check existence of activity |
| val activity = wmState.getActivitiesForWindow(componentMatcher).firstOrNull() |
| check { "\"Activity for window ${componentMatcher.toWindowIdentifier()} exists" } |
| .that(activity) |
| .isNotNull() |
| |
| // Check existence of window. |
| contains(componentMatcher) |
| } |
| |
| /** {@inheritDoc} */ |
| override fun hasRotation(rotation: Rotation, displayId: Int): WindowManagerStateSubject = |
| apply { |
| check { "hasRotation" }.that(wmState.getRotation(displayId)).isEqual(rotation) |
| } |
| |
| /** {@inheritDoc} */ |
| override fun contains(componentMatcher: IComponentMatcher): WindowManagerStateSubject = apply { |
| contains(subjects, componentMatcher) |
| } |
| |
| /** {@inheritDoc} */ |
| override fun notContainsAppWindow( |
| componentMatcher: IComponentMatcher |
| ): WindowManagerStateSubject = apply { |
| // system components (e.g., NavBar, StatusBar, PipOverlay) don't have a package name |
| // nor an activity, ignore them |
| check { "Activity '${componentMatcher.toActivityIdentifier()}' does not exist" } |
| .that(wmState.containsActivity(componentMatcher)) |
| .isEqual(false) |
| notContains(componentMatcher) |
| } |
| |
| /** {@inheritDoc} */ |
| override fun notContains(componentMatcher: IComponentMatcher): WindowManagerStateSubject = |
| apply { |
| check { "Window '${componentMatcher.toWindowIdentifier()}' does not exist" } |
| .that(wmState.containsWindow(componentMatcher)) |
| .isEqual(false) |
| } |
| |
| /** {@inheritDoc} */ |
| override fun isRecentsActivityVisible(): WindowManagerStateSubject = apply { |
| if (wmState.isHomeRecentsComponent) { |
| isHomeActivityVisible() |
| } else { |
| check { "Recents activity is visible" } |
| .that(wmState.isRecentsActivityVisible) |
| .isEqual(true) |
| } |
| } |
| |
| /** {@inheritDoc} */ |
| override fun isRecentsActivityInvisible(): WindowManagerStateSubject = apply { |
| if (wmState.isHomeRecentsComponent) { |
| isHomeActivityInvisible() |
| } else { |
| check { "Recents activity is not visible" } |
| .that(wmState.isRecentsActivityVisible) |
| .isEqual(false) |
| } |
| } |
| |
| /** {@inheritDoc} */ |
| override fun isValid(): WindowManagerStateSubject = apply { |
| check { "Stacks count" }.that(wmState.stackCount).isGreater(0) |
| // TODO: Update when keyguard will be shown on multiple displays |
| if (!wmState.keyguardControllerState.isKeyguardShowing) { |
| check { "Resumed activity" }.that(wmState.resumedActivitiesCount).isGreater(0) |
| } |
| check { "No focused activity" }.that(wmState.focusedActivity).isNotEqual(null) |
| wmState.rootTasks.forEach { aStack -> |
| val stackId = aStack.rootTaskId |
| aStack.tasks.forEach { aTask -> |
| check { "Root task Id for stack $aTask" }.that(stackId).isEqual(aTask.rootTaskId) |
| } |
| } |
| check { "Front window" }.that(wmState.frontWindow).isNotEqual(null) |
| check { "Focused window" }.that(wmState.focusedWindow).isNotEqual(null) |
| check { "Focused app" }.that(wmState.focusedApp.isNotEmpty()).isEqual(true) |
| } |
| |
| /** {@inheritDoc} */ |
| override fun isNonAppWindowVisible( |
| componentMatcher: IComponentMatcher |
| ): WindowManagerStateSubject = apply { checkWindowIsVisible(nonAppWindows, componentMatcher) } |
| |
| /** {@inheritDoc} */ |
| override fun isAppWindowVisible( |
| componentMatcher: IComponentMatcher |
| ): WindowManagerStateSubject = apply { |
| containsAppWindow(componentMatcher) |
| checkWindowIsVisible(appWindows, componentMatcher) |
| } |
| |
| /** {@inheritDoc} */ |
| override fun hasNoVisibleAppWindow(): WindowManagerStateSubject = apply { |
| if (visibleAppWindows.isNotEmpty()) { |
| val visibleAppWindows = visibleAppWindows.joinToString { it.name } |
| fail( |
| Fact(ASSERTION_TAG, "hasNoVisibleAppWindow()"), |
| Fact("Found visible windows", visibleAppWindows) |
| ) |
| } |
| } |
| |
| /** {@inheritDoc} */ |
| override fun isKeyguardShowing(): WindowManagerStateSubject = apply { |
| if (!wmState.isKeyguardShowing && !wmState.isAodShowing) { |
| fail( |
| Fact(ASSERTION_TAG, "isKeyguardShowing()"), |
| Fact("Keyguard showing", wmState.isKeyguardShowing) |
| ) |
| } |
| } |
| |
| /** {@inheritDoc} */ |
| override fun isAppWindowInvisible( |
| componentMatcher: IComponentMatcher |
| ): WindowManagerStateSubject = apply { checkWindowIsInvisible(appWindows, componentMatcher) } |
| |
| /** {@inheritDoc} */ |
| override fun isNonAppWindowInvisible( |
| componentMatcher: IComponentMatcher |
| ): WindowManagerStateSubject = apply { checkWindowIsInvisible(nonAppWindows, componentMatcher) } |
| |
| private fun checkWindowIsVisible( |
| subjectList: List<WindowStateSubject>, |
| componentMatcher: IComponentMatcher |
| ) { |
| // Check existence of window. |
| contains(subjectList, componentMatcher) |
| |
| val foundWindows = |
| subjectList.filter { componentMatcher.windowMatchesAnyOf(it.windowState) } |
| |
| val visibleWindows = |
| wmState.visibleWindows.filter { visibleWindow -> |
| foundWindows.any { it.windowState == visibleWindow } |
| } |
| |
| if (visibleWindows.isEmpty()) { |
| val windowId = componentMatcher.toWindowIdentifier() |
| val facts = |
| listOf(Fact(ASSERTION_TAG, "isVisible($windowId)"), Fact("Is Invisible", windowId)) |
| foundWindows.first().fail(facts) |
| } |
| } |
| |
| private fun checkWindowIsInvisible( |
| subjectList: List<WindowStateSubject>, |
| componentMatcher: IComponentMatcher |
| ) { |
| // Check existence of window. |
| contains(subjectList, componentMatcher) |
| |
| val foundWindows = |
| subjectList.filter { componentMatcher.windowMatchesAnyOf(it.windowState) } |
| |
| val visibleWindows = |
| wmState.visibleWindows.filter { visibleWindow -> |
| foundWindows.any { it.windowState == visibleWindow } |
| } |
| |
| if (visibleWindows.isNotEmpty()) { |
| val windowId = componentMatcher.toWindowIdentifier() |
| val facts = |
| listOf(Fact(ASSERTION_TAG, "isInvisible($windowId)"), Fact("Is Visible", windowId)) |
| foundWindows.first { it.windowState == visibleWindows.first() }.fail(facts) |
| } |
| } |
| |
| private fun contains( |
| subjectList: List<WindowStateSubject>, |
| componentMatcher: IComponentMatcher |
| ) { |
| val windowStates = subjectList.map { it.windowState } |
| check { "Window '${componentMatcher.toWindowIdentifier()}' exists" } |
| .that(componentMatcher.windowMatchesAnyOf(windowStates)) |
| .isEqual(true) |
| } |
| |
| /** {@inheritDoc} */ |
| override fun isHomeActivityVisible(): WindowManagerStateSubject = apply { |
| val homeIsVisible = wmState.homeActivity?.isVisible ?: false |
| check { "Home activity exists" }.that(wmState.homeActivity).isNotEqual(null) |
| check { "Home activity is visible" }.that(homeIsVisible).isEqual(true) |
| } |
| |
| /** {@inheritDoc} */ |
| override fun isHomeActivityInvisible(): WindowManagerStateSubject = apply { |
| val homeIsVisible = wmState.homeActivity?.isVisible ?: false |
| check { "Home activity is not visible" }.that(homeIsVisible).isEqual(false) |
| } |
| |
| /** {@inheritDoc} */ |
| override fun isFocusedApp(app: String): WindowManagerStateSubject = apply { |
| check { "Window is focused app $app" }.that(wmState.focusedApp).isEqual(app) |
| } |
| |
| /** {@inheritDoc} */ |
| override fun isNotFocusedApp(app: String): WindowManagerStateSubject = apply { |
| check { "Window is not focused app $app" }.that(wmState.focusedApp).isNotEqual(app) |
| } |
| |
| /** {@inheritDoc} */ |
| override fun isPinned(componentMatcher: IComponentMatcher): WindowManagerStateSubject = apply { |
| contains(componentMatcher) |
| check { "Window is pinned ${componentMatcher.toWindowIdentifier()}" } |
| .that(wmState.isInPipMode(componentMatcher)) |
| .isEqual(true) |
| } |
| |
| /** {@inheritDoc} */ |
| override fun isNotPinned(componentMatcher: IComponentMatcher): WindowManagerStateSubject = |
| apply { |
| contains(componentMatcher) |
| check { "Window is pinned ${componentMatcher.toWindowIdentifier()}" } |
| .that(wmState.isInPipMode(componentMatcher)) |
| .isEqual(false) |
| } |
| |
| /** {@inheritDoc} */ |
| override fun isAppSnapshotStartingWindowVisibleFor( |
| componentMatcher: IComponentMatcher |
| ): WindowManagerStateSubject = apply { |
| val activity = wmState.getActivitiesForWindow(componentMatcher).firstOrNull() |
| requireNotNull(activity) { "Activity for $componentMatcher not found" } |
| |
| // Check existence and visibility of SnapshotStartingWindow |
| val snapshotStartingWindow = |
| activity.getWindows(ComponentNameMatcher.SNAPSHOT).firstOrNull() |
| |
| check { "SnapshotStartingWindow does not exist for activity ${activity.name}" } |
| .that(snapshotStartingWindow) |
| .isNotEqual(null) |
| check { "Activity is visible" }.that(activity.isVisible).isEqual(true) |
| check { "SnapshotStartingWindow is visible" } |
| .that(snapshotStartingWindow?.isVisible ?: false) |
| .isEqual(true) |
| } |
| |
| /** {@inheritDoc} */ |
| override fun isAboveAppWindowVisible( |
| componentMatcher: IComponentMatcher |
| ): WindowManagerStateSubject = |
| containsAboveAppWindow(componentMatcher).isNonAppWindowVisible(componentMatcher) |
| |
| /** {@inheritDoc} */ |
| override fun isAboveAppWindowInvisible( |
| componentMatcher: IComponentMatcher |
| ): WindowManagerStateSubject = |
| containsAboveAppWindow(componentMatcher).isNonAppWindowInvisible(componentMatcher) |
| |
| /** {@inheritDoc} */ |
| override fun isBelowAppWindowVisible( |
| componentMatcher: IComponentMatcher |
| ): WindowManagerStateSubject = |
| containsBelowAppWindow(componentMatcher).isNonAppWindowVisible(componentMatcher) |
| |
| /** {@inheritDoc} */ |
| override fun isBelowAppWindowInvisible( |
| componentMatcher: IComponentMatcher |
| ): WindowManagerStateSubject = |
| containsBelowAppWindow(componentMatcher).isNonAppWindowInvisible(componentMatcher) |
| |
| /** Obtains the first subject with [WindowState.title] containing [name]. */ |
| fun windowState(name: String): WindowStateSubject? = windowState { it.name.contains(name) } |
| |
| /** |
| * Obtains the first subject matching [predicate]. |
| * |
| * @param predicate to search for a subject |
| */ |
| fun windowState(predicate: (WindowState) -> Boolean): WindowStateSubject? = |
| subjects.firstOrNull { predicate(it.windowState) } |
| |
| override fun toString(): String { |
| return "WindowManagerStateSubject($wmState)" |
| } |
| } |