blob: deb6575eaf76083936f833cecc755e8b178c512c [file] [log] [blame]
/*
* 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.traces.surfaceflinger
import android.tools.common.CrossPlatform
import android.tools.common.ITraceEntry
import android.tools.common.datatypes.Rect
import android.tools.common.datatypes.RectF
import android.tools.common.datatypes.component.ComponentNameMatcher
import android.tools.common.datatypes.component.IComponentMatcher
import kotlin.js.JsExport
import kotlin.js.JsName
/**
* Represents a single Layer trace entry.
*
* This is a generic object that is reused by both Flicker and Winscope and cannot access internal
* Java/Android functionality
*/
@JsExport
class LayerTraceEntry(
@JsName("elapsedTimestamp") val elapsedTimestamp: Long,
@JsName("clockTimestamp") val clockTimestamp: Long?,
@JsName("hwcBlob") val hwcBlob: String,
@JsName("where") val where: String,
@JsName("displays") val displays: Array<Display>,
@JsName("vSyncId") val vSyncId: Long,
_rootLayers: Array<Layer>
) : ITraceEntry {
override val timestamp =
CrossPlatform.timestamp.from(
systemUptimeNanos = elapsedTimestamp,
unixNanos = clockTimestamp
)
@JsName("stableId")
val stableId: String = this::class.simpleName ?: error("Unable to determine class")
@JsName("flattenedLayers") val flattenedLayers: Array<Layer> = fillFlattenedLayers(_rootLayers)
// for winscope
@JsName("isVisible") val isVisible: Boolean = true
@JsName("visibleLayers")
val visibleLayers: Array<Layer>
get() = flattenedLayers.filter { it.isVisible }.toTypedArray()
@JsName("children")
val children: Array<Layer>
get() = flattenedLayers.filter { it.isRootLayer }.toTypedArray()
@JsName("physicalDisplay")
val physicalDisplay: Display?
get() = displays.firstOrNull { !it.isVirtual }
@JsName("physicalDisplayBounds")
val physicalDisplayBounds: Rect?
get() = physicalDisplay?.layerStackSpace
/**
* @return A [Layer] matching [componentMatcher] with a non-empty active buffer, or null if no
* layer matches [componentMatcher] or if the matching layer's buffer is empty
*
* @param componentMatcher Components to search
*/
fun getLayerWithBuffer(componentMatcher: IComponentMatcher): Layer? {
return flattenedLayers.firstOrNull {
componentMatcher.layerMatchesAnyOf(it) && it.activeBuffer.isNotEmpty
}
}
/** @return The [Layer] with [layerId], or null if the layer is not found */
fun getLayerById(layerId: Int): Layer? = this.flattenedLayers.firstOrNull { it.id == layerId }
/**
* Checks if any layer matching [componentMatcher] in the screen is animating.
*
* The screen is animating when a layer is not simple rotation, of when the pip overlay layer is
* visible
*
* @param componentMatcher Components to search
*/
fun isAnimating(
prevState: LayerTraceEntry?,
componentMatcher: IComponentMatcher? = null
): Boolean {
val curLayers =
visibleLayers.filter {
componentMatcher == null || componentMatcher.layerMatchesAnyOf(it)
}
val currIds = visibleLayers.map { it.id }
val prevStateLayers =
prevState?.visibleLayers?.filter { currIds.contains(it.id) } ?: emptyList()
val layersAnimating =
curLayers.any { currLayer ->
val prevLayer = prevStateLayers.firstOrNull { it.id == currLayer.id }
currLayer.isAnimating(prevLayer)
}
val pipAnimating = isVisible(ComponentNameMatcher.PIP_CONTENT_OVERLAY)
return layersAnimating || pipAnimating
}
/**
* Check if at least one window matching [componentMatcher] is visible.
*
* @param componentMatcher Components to search
*/
@JsName("isVisibleComponent")
fun isVisible(componentMatcher: IComponentMatcher): Boolean =
componentMatcher.layerMatchesAnyOf(visibleLayers)
/** @return A [LayersTrace] object containing this state as its only entry */
fun asTrace(): LayersTrace = LayersTrace(arrayOf(this))
override fun toString(): String = "${timestamp}ns"
override fun equals(other: Any?): Boolean {
return other is LayerTraceEntry && other.timestamp == this.timestamp
}
override fun hashCode(): Int {
var result = timestamp.hashCode()
result = 31 * result + hwcBlob.hashCode()
result = 31 * result + where.hashCode()
result = 31 * result + displays.contentHashCode()
result = 31 * result + isVisible.hashCode()
result = 31 * result + flattenedLayers.contentHashCode()
return result
}
private fun fillFlattenedLayers(rootLayers: Array<Layer>): Array<Layer> {
val layers = mutableListOf<Layer>()
val roots = rootLayers.fillOcclusionState().toMutableList()
while (roots.isNotEmpty()) {
val layer = roots.removeAt(0)
layers.add(layer)
roots.addAll(layer.children)
}
return layers.toTypedArray()
}
private fun Array<Layer>.topDownTraversal(): List<Layer> {
return this.sortedBy { it.z }.flatMap { it.topDownTraversal() }
}
private fun Layer.topDownTraversal(): List<Layer> {
val traverseList = mutableListOf(this)
this.children
.sortedBy { it.z }
.forEach { childLayer -> traverseList.addAll(childLayer.topDownTraversal()) }
return traverseList
}
private fun Array<Layer>.fillOcclusionState(): Array<Layer> {
val traversalList = topDownTraversal().reversed()
val opaqueLayers = mutableListOf<Layer>()
val transparentLayers = mutableListOf<Layer>()
traversalList.forEach { layer ->
val visible = layer.isVisible
val displaySize =
displays
.firstOrNull { it.layerStackId == layer.stackId }
?.layerStackSpace
?.toRectF()
?: RectF.EMPTY
if (visible) {
val occludedBy =
opaqueLayers
.filter {
it.stackId == layer.stackId &&
it.contains(layer, displaySize) &&
(!it.hasRoundedCorners || (layer.cornerRadius == it.cornerRadius))
}
.toTypedArray()
layer.addOccludedBy(occludedBy)
val partiallyOccludedBy =
opaqueLayers
.filter {
it.stackId == layer.stackId &&
it.overlaps(layer, displaySize) &&
it !in layer.occludedBy
}
.toTypedArray()
layer.addPartiallyOccludedBy(partiallyOccludedBy)
val coveredBy =
transparentLayers
.filter { it.stackId == layer.stackId && it.overlaps(layer, displaySize) }
.toTypedArray()
layer.addCoveredBy(coveredBy)
if (layer.isOpaque) {
opaqueLayers.add(layer)
} else {
transparentLayers.add(layer)
}
}
}
return this
}
}