1/* 2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17import {Timestamp, TimestampType} from 'trace/timestamp'; 18import {TraceFile} from 'trace/trace_file'; 19import {TraceType} from 'trace/trace_type'; 20import {AbstractParser} from './abstract_parser'; 21import {ExportedData} from './proto_types'; 22 23/* TODO: Support multiple Windows in one file upload. */ 24export class ParserViewCapture extends AbstractParser { 25 private classNames: string[] = []; 26 private realToElapsedTimeOffsetNanos: bigint | undefined = undefined; 27 packageName: string = ''; 28 windowTitle: string = ''; 29 30 constructor(trace: TraceFile) { 31 super(trace); 32 } 33 34 override getTraceType(): TraceType { 35 return TraceType.VIEW_CAPTURE; 36 } 37 38 override getMagicNumber(): number[] { 39 return ParserViewCapture.MAGIC_NUMBER; 40 } 41 42 override decodeTrace(buffer: Uint8Array): any[] { 43 const exportedData = ExportedData.decode(buffer) as any; 44 this.classNames = exportedData.classname; 45 this.realToElapsedTimeOffsetNanos = BigInt(exportedData.realToElapsedTimeOffsetNanos); 46 this.packageName = this.shortenAndCapitalize(exportedData.package); 47 48 const firstWindowData = exportedData.windowData[0]; 49 this.windowTitle = this.shortenAndCapitalize(firstWindowData.title); 50 51 return firstWindowData.frameData; 52 } 53 54 override processDecodedEntry(index: number, timestampType: TimestampType, decodedEntry: any) { 55 this.formatProperties(decodedEntry.node, this.classNames); 56 return decodedEntry; 57 } 58 59 private shortenAndCapitalize(name: string): string { 60 const shortName = name.substring(name.lastIndexOf('.') + 1); 61 return shortName.charAt(0).toUpperCase() + shortName.slice(1); 62 } 63 64 private formatProperties(root: any /* ViewNode */, classNames: string[]): any /* ViewNode */ { 65 const DEPTH_MAGNIFICATION = 4; 66 const VISIBLE = 0; 67 68 function inner( 69 node: any /* ViewNode */, 70 leftShift: number, 71 topShift: number, 72 scaleX: number, 73 scaleY: number, 74 depth: number, 75 isParentVisible: boolean 76 ) { 77 const newScaleX = scaleX * node.scaleX; 78 const newScaleY = scaleY * node.scaleY; 79 80 const l = 81 leftShift + 82 (node.left + node.translationX) * scaleX + 83 (node.width * (scaleX - newScaleX)) / 2; 84 const t = 85 topShift + 86 (node.top + node.translationY) * scaleY + 87 (node.height * (scaleY - newScaleY)) / 2; 88 node.boxPos = { 89 left: l, 90 top: t, 91 width: node.width * newScaleX, 92 height: node.height * newScaleY, 93 }; 94 95 node.name = `${classNames[node.classnameIndex]}@${node.hashcode}`; 96 97 node.shortName = node.name.split('.'); 98 node.shortName = node.shortName[node.shortName.length - 1]; 99 100 node.isVisible = isParentVisible && VISIBLE === node.visibility; 101 102 for (let i = 0; i < node.children.length; i++) { 103 inner( 104 node.children[i], 105 l - node.scrollX, 106 t - node.scrollY, 107 newScaleX, 108 newScaleY, 109 depth + 1, 110 node.isVisible 111 ); 112 node.children[i].parent = node; 113 } 114 115 // TODO: Audit these properties 116 node.depth = depth * DEPTH_MAGNIFICATION; 117 node.type = 'ViewNode'; 118 node.layerId = 0; 119 node.isMissing = false; 120 node.hwcCompositionType = 0; 121 node.zOrderRelativeOfId = -1; 122 node.isRootLayer = false; 123 node.skip = null; 124 node.id = node.name; 125 node.stableId = node.id; 126 node.equals = (other: any /* ViewNode */) => ParserViewCapture.equals(node, other); 127 } 128 129 root.scaleX = root.scaleY = 1; 130 root.translationX = root.translationY = 0; 131 inner(root, 0, 0, 1, 1, 0, true); 132 133 root.isRootLayer = true; 134 return root; 135 } 136 137 override getTimestamp(timestampType: TimestampType, frameData: any): undefined | Timestamp { 138 return Timestamp.from( 139 timestampType, 140 BigInt(frameData.timestamp), 141 this.realToElapsedTimeOffsetNanos 142 ); 143 } 144 145 private static readonly MAGIC_NUMBER = [0x9, 0x78, 0x65, 0x90, 0x65, 0x73, 0x82, 0x65, 0x68]; 146 147 /** This method is used by the tree_generator to determine if 2 nodes have equivalent properties. */ 148 private static equals(node: any /* ViewNode */, other: any /* ViewNode */): boolean { 149 if (!node && !other) { 150 return true; 151 } 152 if (!node || !other) { 153 return false; 154 } 155 return ( 156 node.id === other.id && 157 node.name === other.name && 158 node.hashcode === other.hashcode && 159 node.left === other.left && 160 node.top === other.top && 161 node.height === other.height && 162 node.width === other.width && 163 node.elevation === other.elevation && 164 node.scaleX === other.scaleX && 165 node.scaleY === other.scaleY && 166 node.scrollX === other.scrollX && 167 node.scrollY === other.scrollY && 168 node.translationX === other.translationX && 169 node.translationY === other.translationY && 170 node.alpha === other.alpha && 171 node.visibility === other.visibility && 172 node.willNotDraw === other.willNotDraw && 173 node.clipChildren === other.clipChildren && 174 node.depth === other.depth 175 ); 176 } 177} 178