1/* 2 * Copyright 2021, 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 {toBounds, toBuffer, toColor, toPoint, toRect, 18 toRectF, toRegion, toTransform} from './common'; 19import intDefMapping from 20 '../../../../../prebuilts/misc/common/winscope/intDefMapping.json'; 21 22export default class ObjectFormatter { 23 private static INVALID_ELEMENT_PROPERTIES = ['length', 'name', 'prototype', 'children', 24 'childrenWindows', 'ref', 'root', 'layers', 'resolvedChildren'] 25 26 private static FLICKER_INTDEF_MAP = new Map([ 27 [`WindowLayoutParams.type`, `android.view.WindowManager.LayoutParams.WindowType`], 28 [`WindowLayoutParams.flags`, `android.view.WindowManager.LayoutParams.Flags`], 29 [`WindowLayoutParams.privateFlags`, `android.view.WindowManager.LayoutParams.PrivateFlags`], 30 [`WindowLayoutParams.gravity`, `android.view.Gravity.GravityFlags`], 31 [`WindowLayoutParams.softInputMode`, `android.view.WindowManager.LayoutParams.WindowType`], 32 [`WindowLayoutParams.systemUiVisibilityFlags`, `android.view.WindowManager.LayoutParams.SystemUiVisibilityFlags`], 33 [`WindowLayoutParams.subtreeSystemUiVisibilityFlags`, `android.view.WindowManager.LayoutParams.SystemUiVisibilityFlags`], 34 [`WindowLayoutParams.behavior`, `android.view.WindowInsetsController.Behavior`], 35 [`WindowLayoutParams.fitInsetsSides`, `android.view.WindowInsets.Side.InsetsSide`], 36 37 [`Configuration.windowingMode`, `android.app.WindowConfiguration.WindowingMode`], 38 [`WindowConfiguration.windowingMode`, `android.app.WindowConfiguration.WindowingMode`], 39 [`Configuration.orientation`, `android.content.pm.ActivityInfo.ScreenOrientation`], 40 [`WindowConfiguration.orientation`, `android.content.pm.ActivityInfo.ScreenOrientation`], 41 [`WindowState.orientation`, `android.content.pm.ActivityInfo.ScreenOrientation`], 42 ]) 43 44 static format(obj: any): {} { 45 const entries = Object.entries(obj) 46 .filter(it => !it[0].includes(`$`)) 47 .filter(it => !this.INVALID_ELEMENT_PROPERTIES.includes(it[0])) 48 const sortedEntries = entries.sort() 49 50 const result: any = {} 51 sortedEntries.forEach(entry => { 52 const key = entry[0] 53 const value: any = entry[1] 54 55 if (value) { 56 // flicker obj 57 if (value.prettyPrint) { 58 result[key] = value.prettyPrint() 59 } else { 60 // converted proto to flicker 61 const translatedObject = this.translateObject(value) 62 if (translatedObject) { 63 result[key] = translatedObject.prettyPrint() 64 // objects - recursive call 65 } else if (value && typeof(value) == `object`) { 66 result[key] = this.format(value) 67 } else { 68 // values 69 result[key] = this.translateIntDef(obj, key, value) 70 } 71 } 72 73 } 74 }) 75 76 return Object.freeze(result) 77 } 78 79 /** 80 * Translate some predetermined proto objects into their flicker equivalent 81 * 82 * Returns null if the object cannot be translated 83 * 84 * @param obj Object to translate 85 */ 86 private static translateObject(obj) { 87 const type = obj?.$type?.name 88 switch(type) { 89 case `SizeProto`: return toBounds(obj) 90 case `ActiveBufferProto`: return toBuffer(obj) 91 case `ColorProto`: return toColor(obj) 92 case `PointProto`: return toPoint(obj) 93 case `RectProto`: return toRect(obj) 94 case `FloatRectProto`: return toRectF(obj) 95 case `RegionProto`: return toRegion(obj) 96 case `TransformProto`: return toTransform(obj) 97 case 'ColorTransformProto': { 98 const formatted = this.formatColorTransform(obj.val); 99 return `${formatted}`; 100 } 101 } 102 103 return null 104 } 105 106 private static formatColorTransform(vals) { 107 const fixedVals = vals.map((v) => v.toFixed(1)); 108 let formatted = ``; 109 for (let i = 0; i < fixedVals.length; i += 4) { 110 formatted += `[`; 111 formatted += fixedVals.slice(i, i + 4).join(', '); 112 formatted += `] `; 113 } 114 return formatted; 115 } 116 117 /** 118 * Obtains from the proto field, the metadata related to the typedef type (if any) 119 * 120 * @param obj Proto object 121 * @param propertyName Property to search 122 */ 123 private static getTypeDefSpec(obj: any, propertyName: string): string { 124 const fields = obj?.$type?.fields 125 if (!fields) { 126 return null 127 } 128 129 const options = fields[propertyName]?.options 130 if (!options) { 131 return null 132 } 133 134 return options["(.android.typedef)"] 135 } 136 137 /** 138 * Translate intdef properties into their string representation 139 * 140 * For proto objects check the 141 * 142 * @param parentObj Object containing the value to parse 143 * @param propertyName Property to search 144 * @param value Property value 145 */ 146 private static translateIntDef(parentObj: any, propertyName: string, value: any): string { 147 const parentClassName = parentObj.constructor.name 148 const propertyPath = `${parentClassName}.${propertyName}` 149 150 let translatedValue = value 151 // Parse Flicker objects (no intdef annotation supported) 152 if (this.FLICKER_INTDEF_MAP.has(propertyPath)) { 153 translatedValue = this.getIntFlagsAsStrings(value, 154 this.FLICKER_INTDEF_MAP.get(propertyPath)) 155 } else { 156 // If it's a proto, search on the proto definition for the intdef type 157 const typeDefSpec = this.getTypeDefSpec(parentObj, propertyName) 158 if (typeDefSpec) { 159 translatedValue = this.getIntFlagsAsStrings(value, typeDefSpec) 160 } 161 } 162 163 return translatedValue 164 } 165 166 /** 167 * Translate a property from its numerical value into its string representation 168 * 169 * @param intFlags Property value 170 * @param annotationType IntDef type to use 171 */ 172 private static getIntFlagsAsStrings(intFlags: any, annotationType: string) { 173 const flags = []; 174 175 const mapping = intDefMapping[annotationType].values; 176 const knownFlagValues = Object.keys(mapping).reverse().map(x => parseInt(x)); 177 178 if (mapping.length == 0) { 179 console.warn("No mapping for type", annotationType) 180 return intFlags + "" 181 } 182 183 // Will only contain bits that have not been associated with a flag. 184 const parsedIntFlags = parseInt(intFlags); 185 let leftOver = parsedIntFlags; 186 187 for (const flagValue of knownFlagValues) { 188 if (((leftOver & flagValue) && ((intFlags & flagValue) === flagValue)) 189 || (parsedIntFlags === 0 && flagValue === 0)) { 190 flags.push(mapping[flagValue]); 191 192 leftOver = leftOver & ~flagValue; 193 } 194 } 195 196 if (flags.length === 0) { 197 console.error('No valid flag mappings found for ', 198 intFlags, 'of type', annotationType); 199 } 200 201 if (leftOver) { 202 // If 0 is a valid flag value that isn't in the intDefMapping 203 // it will be ignored 204 flags.push(leftOver); 205 } 206 207 return flags.join(' | '); 208 } 209}