blob: cf27bd49dbf0ab350d02aafcfd68ed3fb9ced38a [file] [log] [blame]
/*
* Copyright (C) 2020 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.server.wm.flicker
import android.graphics.Region
import android.graphics.nano.RectProto
import com.android.server.wm.nano.WindowContainerChildProto
import com.android.server.wm.nano.WindowContainerProto
import com.android.server.wm.nano.WindowManagerTraceProto
import com.android.server.wm.nano.WindowStateProto
/** Represents a single WindowManager trace entry. */
class WindowManagerTraceEntry(val proto: WindowManagerTraceProto) : ITraceEntry {
private var _appWindows = mutableSetOf<WindowStateProto>()
override val timestamp by lazy {
proto.elapsedRealtimeNanos
}
/**
* Returns all windows in the hierarchy
*/
val windows = getWindows(
proto.windowManagerService.rootWindowContainer.windowContainer,
isAppWindow = false
).toSet()
/**
* Return the app windows in the hierarchy.
* A window is considered an app window if any of its ancestors is an activity record
*/
val appWindows = _appWindows.toSet()
/**
* Returns the non app windows in the hierarchy.
* A window is considered a non app window if none of its ancestors is an activity record
*/
val nonAppWindows by lazy {
windows - appWindows
}
/**
* Returns the non app windows in the hierarchy that appear before the first app window.
*/
val aboveAppWindows: List<WindowStateProto> by lazy {
windows.takeWhile { !appWindows.contains(it) }
}
/**
* Returns the non app windows in the hierarchy that appear after the first app window.
*/
val belowAppWindows: List<WindowStateProto> by lazy {
windows.dropWhile { !appWindows.contains(it) }
.drop(appWindows.size)
}
/**
* Returns window title of the top most visible app window.
*/
val topVisibleAppWindow by lazy {
appWindows.filter { it.isVisible() }
.map { it.title }
.firstOrNull() ?: ""
}
/**
* Returns all visible windows in the hierarchy
*/
val visibleWindows by lazy {
windows.filter { it.isVisible() }
}
private fun getWindows(windowContainer: WindowContainerProto, isAppWindow: Boolean)
= windowContainer.children.reversed().flatMap { getWindows(it, isAppWindow) }
private fun getWindows(
windowContainer: WindowContainerChildProto,
isAppWindow: Boolean
): List<WindowStateProto> {
return if (windowContainer.displayArea != null) {
getWindows(windowContainer.displayArea.windowContainer, isAppWindow)
} else if (windowContainer.displayContent != null
&& windowContainer.displayContent.windowContainer == null) {
getWindows(windowContainer.displayContent.rootDisplayArea.windowContainer, isAppWindow)
} else if (windowContainer.displayContent != null) {
getWindows(windowContainer.displayContent.windowContainer, isAppWindow)
} else if (windowContainer.task != null) {
getWindows(windowContainer.task.windowContainer, isAppWindow)
} else if (windowContainer.activity != null) {
getWindows(windowContainer.activity.windowToken.windowContainer, true)
} else if (windowContainer.windowToken != null) {
getWindows(windowContainer.windowToken.windowContainer, isAppWindow)
} else if (windowContainer.window != null) {
if (isAppWindow) {
_appWindows.add(windowContainer.window)
}
listOf(windowContainer.window)
} else {
getWindows(windowContainer.windowContainer, isAppWindow)
}
}
/** Checks if non app window with `windowTitle` is visible. */
private fun getWindowByIdentifier(
windowState: WindowStateProto,
windowTitle: String
): WindowStateProto? {
return if (windowState.title.contains(windowTitle)) {
windowState
} else windowState.childWindows
.firstOrNull { getWindowByIdentifier(it, windowTitle) != null }
}
private fun Collection<WindowStateProto>.isWindowVisible(
assertionName: String,
windowTitle: String,
isVisible: Boolean = true
): AssertionResult {
val foundWindow = this.filter { getWindowByIdentifier(it, windowTitle) != null }
return when {
this.isEmpty() -> return AssertionResult(
"No windows found",
assertionName,
timestamp,
success = false)
foundWindow.isEmpty() -> AssertionResult(
"$windowTitle cannot be found",
assertionName,
timestamp,
success = false)
isVisible && foundWindow.none { it.isVisible() } -> AssertionResult(
"$windowTitle is invisible",
assertionName,
timestamp,
success = false)
!isVisible && foundWindow.any { it.isVisible() } -> AssertionResult(
"$windowTitle is visible",
assertionName,
timestamp,
success = false)
else -> {
val reason = if (isVisible) {
"${foundWindow.first { it.isVisible() }.title} is visible"
} else {
"${foundWindow.first { !it.isVisible() }.title} is invisible"
}
AssertionResult(
success = true,
reason = reason)
}
}
}
/**
* Checks if the non-app window with title containing [windowTitle] exists above the app
* windows and if its visibility is equal to [isVisible]
*
* @param windowTitle window title to search
* @param isVisible if the found window should be visible or not
*/
@JvmOverloads
fun isAboveAppWindow(windowTitle: String, isVisible: Boolean = true): AssertionResult {
return aboveAppWindows.isWindowVisible(
"isAboveAppWindow${if (isVisible) "Visible" else "Invisible"}",
windowTitle,
isVisible)
}
/**
* Checks if the non-app window with title containing [windowTitle] exists below the app windows
* and if its visibility is equal to [isVisible]
*
* @param windowTitle window title to search
* @param isVisible if the found window should be visible or not
*/
@JvmOverloads
fun isBelowAppWindow(windowTitle: String, isVisible: Boolean = true): AssertionResult {
return belowAppWindows.isWindowVisible(
"isBelowAppWindow${if (isVisible) "Visible" else "Invisible"}",
windowTitle,
isVisible)
}
/**
* Checks if non-app window with title containing the [windowTitle] exists above or below the
* app windows and if its visibility is equal to [isVisible]
*
* @param windowTitle window title to search
* @param isVisible if the found window should be visible or not
*/
@JvmOverloads
fun hasNonAppWindow(windowTitle: String, isVisible: Boolean = true): AssertionResult {
return nonAppWindows.isWindowVisible(
"isAppWindowVisible", windowTitle, isVisible)
}
/**
* Checks if app window with title containing the [windowTitle] is on top
*
* @param windowTitle window title to search
*/
fun isVisibleAppWindowOnTop(windowTitle: String): AssertionResult {
val success = topVisibleAppWindow.contains(windowTitle)
val reason = "wanted=$windowTitle found=$topVisibleAppWindow"
return AssertionResult(reason, "isAppWindowOnTop", timestamp, success)
}
/**
* Checks if app window with title containing the [windowTitle] is visible
*
* @param windowTitle window title to search
*/
fun isAppWindowVisible(windowTitle: String): AssertionResult {
return appWindows.isWindowVisible("isAppWindowVisible",
windowTitle,
isVisible = true)
}
/**
* Obtains the first window with title containing [windowTitle] is visible.
* Computes the result based on the intersection between the [testRegion] and the window frame.
*
* @param windowTitle Name of the layer to search
* @param testRegion Expected visible area of the window
* @param resultComputation Predicate to compute a result based on the found window's region
*/
private fun covers(
windowTitle: String,
testRegion: Region,
resultComputation: (Region) -> AssertionResult
): AssertionResult {
val assertionName = "covers"
val visibilityCheck = windows.isWindowVisible(assertionName, windowTitle)
if (!visibilityCheck.success) {
return visibilityCheck
}
val foundWindow = windows.first { getWindowByIdentifier(it, windowTitle) != null }
val foundRegion = foundWindow.frameRegion
val testRect = testRegion.bounds
return resultComputation(foundRegion)
}
/**
* Checks if the first window with title containing [windowTitle] has a visible area of at
* least [testRegion], that is, if its area of the window frame covers each point in
* the region.
*
* @param windowTitle Name of the layer to search
* @param testRegion Expected visible area of the window
*/
fun coversAtLeastRegion(windowTitle: String, testRegion: Region): AssertionResult {
return covers(windowTitle, testRegion) { windowRegion ->
val testRect = testRegion.bounds
val intersection = Region(windowRegion)
val covers = intersection.op(testRect, Region.Op.INTERSECT)
&& !intersection.op(testRect, Region.Op.XOR)
val reason = if (covers) {
"$windowTitle covers region $testRegion"
} else {
("Region to test: $testRegion"
+ "\nUncovered region: $intersection")
}
AssertionResult(reason, "coversAtLeastRegion", timestamp, success = true)
}
}
/**
* Checks if the first window with title containing [windowTitle] has a visible area of at
* most [testRegion], that is, if the region covers each point in the window frame.
*
* @param windowTitle Name of the layer to search
* @param testRegion Expected visible area of the window
*/
fun coversAtMostRegion(windowTitle: String, testRegion: Region): AssertionResult {
return covers(windowTitle, testRegion) { windowRegion ->
val testRect = testRegion.bounds
val intersection = Region(windowRegion)
val covers = intersection.op(testRect, Region.Op.INTERSECT)
&& !intersection.op(windowRegion, Region.Op.XOR)
val reason = if (covers) {
"$windowTitle covers region $testRegion"
} else {
("Region to test: $testRegion"
+ "\nOut-of-bounds region: $intersection")
}
AssertionResult(reason, "coversAtMostRegion", timestamp, success = true)
}
}
private fun WindowStateProto.isVisible(): Boolean = this.windowContainer.visible
private val WindowStateProto.title: String
get() = this.windowContainer.identifier.title
private val WindowStateProto.frameRegion: Region
get() = if (this.windowFrames != null) {
this.windowFrames.frame.extract()
} else {
this.frame.extract()
}
private fun RectProto?.extract(): Region {
return if (this == null) {
Region()
} else {
Region(this.left, this.top, this.right, this.bottom)
}
}
}