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 {toSize, toBuffer, toColor, toPoint, toRect, 18 toRectF, toRegion, toTransform} from './common'; 19import intDefMapping from 20 '../../../../../prebuilts/misc/common/winscope/intDefMapping.json'; 21import config from '../config/Configuration.json' 22 23function readIntdefMap(): Map<string, string> { 24 const map = new Map<string, string>(); 25 const keys = Object.keys(config.intDefColumn); 26 27 keys.forEach(key => { 28 const value = config.intDefColumn[key]; 29 map.set(key, value); 30 }); 31 32 return map; 33} 34export default class ObjectFormatter { 35 static displayDefaults: boolean = false 36 private static INVALID_ELEMENT_PROPERTIES = config.invalidProperties; 37 38 private static FLICKER_INTDEF_MAP = readIntdefMap(); 39 40 static cloneObject(entry: any): any { 41 let obj: any = {} 42 const properties = ObjectFormatter.getProperties(entry); 43 properties.forEach(prop => obj[prop] = entry[prop]); 44 return obj; 45 } 46 47 /** 48 * Get the true properties of an entry excluding functions, kotlin gernerated 49 * variables, explicitly excluded properties, and flicker objects already in 50 * the hierarchy that shouldn't be traversed when formatting the entry 51 * @param entry The entry for which we want to get the properties for 52 * @return The "true" properties of the entry as described above 53 */ 54 static getProperties(entry: any): string[] { 55 var props = []; 56 let obj = entry; 57 58 do { 59 const properties = Object.getOwnPropertyNames(obj).filter(it => { 60 // filter out functions 61 if (typeof(entry[it]) === 'function') return false; 62 // internal propertires from kotlinJs 63 if (it.includes(`$`)) return false; 64 // private kotlin variables from kotlin 65 if (it.startsWith(`_`)) return false; 66 // some predefined properties used only internally (e.g., children, ref, diff) 67 if (this.INVALID_ELEMENT_PROPERTIES.includes(it)) return false; 68 69 const value = entry[it]; 70 // only non-empty arrays of non-flicker objects (otherwise they are in hierarchy) 71 if (Array.isArray(value) && value.length > 0) return !value[0].stableId; 72 // non-flicker object 73 return !(value?.stableId); 74 }); 75 properties.forEach(function (prop) { 76 if (typeof(entry[prop]) !== 'function' && props.indexOf(prop) === -1) { 77 props.push(prop); 78 } 79 }); 80 } while (obj = Object.getPrototypeOf(obj)); 81 82 return props; 83 } 84 85 /** 86 * Format a Winscope entry to be displayed in the UI 87 * Accounts for different user display settings (e.g. hiding empty/default values) 88 * @param obj The raw object to format 89 * @return The formatted object 90 */ 91 static format(obj: any): {} { 92 const properties = this.getProperties(obj); 93 const sortedProperties = properties.sort() 94 95 const result: any = {} 96 sortedProperties.forEach(entry => { 97 const key = entry; 98 const value: any = obj[key]; 99 100 if (value === null || value === undefined) { 101 if (this.displayDefaults) { 102 result[key] = value 103 } 104 return 105 } 106 107 if (value || this.displayDefaults) { 108 // flicker obj 109 if (value.prettyPrint) { 110 const isEmpty = value.isEmpty === true; 111 if (!isEmpty || this.displayDefaults) { 112 result[key] = value.prettyPrint() 113 } 114 } else { 115 // converted proto to flicker 116 const translatedObject = this.translateObject(value) 117 if (translatedObject) { 118 result[key] = translatedObject.prettyPrint() 119 // objects - recursive call 120 } else if (value && typeof(value) == `object`) { 121 const childObj = this.format(value) as any 122 const isEmpty = Object.entries(childObj).length == 0 || childObj.isEmpty 123 if (!isEmpty || this.displayDefaults) { 124 result[key] = childObj 125 } 126 } else { 127 // values 128 result[key] = this.translateIntDef(obj, key, value) 129 } 130 } 131 132 } 133 }) 134 135 // return Object.freeze(result) 136 return result 137 } 138 139 /** 140 * Translate some predetermined proto objects into their flicker equivalent 141 * 142 * Returns null if the object cannot be translated 143 * 144 * @param obj Object to translate 145 */ 146 private static translateObject(obj) { 147 const type = obj?.$type?.name 148 switch(type) { 149 case `SizeProto`: return toSize(obj) 150 case `ActiveBufferProto`: return toBuffer(obj) 151 case `ColorProto`: return toColor(obj) 152 case `PointProto`: return toPoint(obj) 153 case `RectProto`: return toRect(obj) 154 case `FloatRectProto`: return toRectF(obj) 155 case `RegionProto`: return toRegion(obj) 156 case `TransformProto`: return toTransform(obj) 157 case 'ColorTransformProto': { 158 const formatted = this.formatColorTransform(obj.val); 159 return `${formatted}`; 160 } 161 } 162 163 return null 164 } 165 166 private static formatColorTransform(vals) { 167 const fixedVals = vals.map((v) => v.toFixed(1)); 168 let formatted = ``; 169 for (let i = 0; i < fixedVals.length; i += 4) { 170 formatted += `[`; 171 formatted += fixedVals.slice(i, i + 4).join(', '); 172 formatted += `] `; 173 } 174 return formatted; 175 } 176 177 /** 178 * Obtains from the proto field, the metadata related to the typedef type (if any) 179 * 180 * @param obj Proto object 181 * @param propertyName Property to search 182 */ 183 private static getTypeDefSpec(obj: any, propertyName: string): string { 184 const fields = obj?.$type?.fields 185 if (!fields) { 186 return null 187 } 188 189 const options = fields[propertyName]?.options 190 if (!options) { 191 return null 192 } 193 194 return options["(.android.typedef)"] 195 } 196 197 /** 198 * Translate intdef properties into their string representation 199 * 200 * For proto objects check the 201 * 202 * @param parentObj Object containing the value to parse 203 * @param propertyName Property to search 204 * @param value Property value 205 */ 206 private static translateIntDef(parentObj: any, propertyName: string, value: any): string { 207 const parentClassName = parentObj.constructor.name 208 const propertyPath = `${parentClassName}.${propertyName}` 209 210 let translatedValue = value 211 // Parse Flicker objects (no intdef annotation supported) 212 if (this.FLICKER_INTDEF_MAP.has(propertyPath)) { 213 translatedValue = this.getIntFlagsAsStrings(value, 214 this.FLICKER_INTDEF_MAP.get(propertyPath)) 215 } else { 216 // If it's a proto, search on the proto definition for the intdef type 217 const typeDefSpec = this.getTypeDefSpec(parentObj, propertyName) 218 if (typeDefSpec) { 219 translatedValue = this.getIntFlagsAsStrings(value, typeDefSpec) 220 } 221 } 222 223 return translatedValue 224 } 225 226 /** 227 * Translate a property from its numerical value into its string representation 228 * 229 * @param intFlags Property value 230 * @param annotationType IntDef type to use 231 */ 232 private static getIntFlagsAsStrings(intFlags: any, annotationType: string) { 233 const flags = []; 234 235 const mapping = intDefMapping[annotationType].values; 236 const knownFlagValues = Object.keys(mapping).reverse().map(x => parseInt(x)); 237 238 if (mapping.length == 0) { 239 console.warn("No mapping for type", annotationType) 240 return intFlags + "" 241 } 242 243 // Will only contain bits that have not been associated with a flag. 244 const parsedIntFlags = parseInt(intFlags); 245 let leftOver = parsedIntFlags; 246 247 for (const flagValue of knownFlagValues) { 248 if (((leftOver & flagValue) && ((intFlags & flagValue) === flagValue)) 249 || (parsedIntFlags === 0 && flagValue === 0)) { 250 flags.push(mapping[flagValue]); 251 252 leftOver = leftOver & ~flagValue; 253 } 254 } 255 256 if (flags.length === 0) { 257 console.error('No valid flag mappings found for ', 258 intFlags, 'of type', annotationType); 259 } 260 261 if (leftOver) { 262 // If 0 is a valid flag value that isn't in the intDefMapping 263 // it will be ignored 264 flags.push(leftOver); 265 } 266 267 return flags.join(' | '); 268 } 269} 270