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, toActiveBuffer, toColor, toColor3, toPoint, toRect, 18 toRectF, toRegion, toMatrix22, 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 // raw values (e.g., false or 0) 109 if (!value) { 110 result[key] = value 111 // flicker obj 112 } else if (value.prettyPrint) { 113 const isEmpty = value.isEmpty === true; 114 if (!isEmpty || this.displayDefaults) { 115 result[key] = value.prettyPrint(); 116 } 117 } else { 118 // converted proto to flicker 119 const translatedObject = this.translateObject(value); 120 if (translatedObject) { 121 if (translatedObject.prettyPrint) { 122 result[key] = translatedObject.prettyPrint(); 123 } 124 else { 125 result[key] = translatedObject; 126 } 127 // objects - recursive call 128 } else if (value && typeof(value) == `object`) { 129 const childObj = this.format(value) as any; 130 const isEmpty = Object.entries(childObj).length == 0 || childObj.isEmpty; 131 if (!isEmpty || this.displayDefaults) { 132 result[key] = childObj; 133 } 134 } else { 135 // values 136 result[key] = this.translateIntDef(obj, key, value); 137 } 138 } 139 140 } 141 }) 142 143 return result; 144 } 145 146 /** 147 * Translate some predetermined proto objects into their flicker equivalent 148 * 149 * Returns null if the object cannot be translated 150 * 151 * @param obj Object to translate 152 */ 153 private static translateObject(obj) { 154 const type = obj?.$type?.name; 155 switch(type) { 156 case `SizeProto`: return toSize(obj); 157 case `ActiveBufferProto`: return toActiveBuffer(obj); 158 case `Color3`: return toColor3(obj); 159 case `ColorProto`: return toColor(obj); 160 case `PointProto`: return toPoint(obj); 161 case `RectProto`: return toRect(obj); 162 case `Matrix22`: return toMatrix22(obj); 163 case `FloatRectProto`: return toRectF(obj); 164 case `RegionProto`: return toRegion(obj); 165 case `TransformProto`: return toTransform(obj); 166 case 'ColorTransformProto': { 167 const formatted = this.formatColorTransform(obj.val); 168 return `${formatted}`; 169 } 170 } 171 172 return null; 173 } 174 175 private static formatColorTransform(vals) { 176 const fixedVals = vals.map((v) => v.toFixed(1)); 177 let formatted = ``; 178 for (let i = 0; i < fixedVals.length; i += 4) { 179 formatted += `[`; 180 formatted += fixedVals.slice(i, i + 4).join(', '); 181 formatted += `] `; 182 } 183 return formatted; 184 } 185 186 /** 187 * Obtains from the proto field, the metadata related to the typedef type (if any) 188 * 189 * @param obj Proto object 190 * @param propertyName Property to search 191 */ 192 private static getTypeDefSpec(obj: any, propertyName: string): string { 193 const fields = obj?.$type?.fields; 194 if (!fields) { 195 return null; 196 } 197 198 const options = fields[propertyName]?.options; 199 if (!options) { 200 return null; 201 } 202 203 return options["(.android.typedef)"]; 204 } 205 206 /** 207 * Translate intdef properties into their string representation 208 * 209 * For proto objects check the 210 * 211 * @param parentObj Object containing the value to parse 212 * @param propertyName Property to search 213 * @param value Property value 214 */ 215 private static translateIntDef(parentObj: any, propertyName: string, value: any): string { 216 const parentClassName = parentObj.constructor.name; 217 const propertyPath = `${parentClassName}.${propertyName}`; 218 219 let translatedValue = value; 220 // Parse Flicker objects (no intdef annotation supported) 221 if (this.FLICKER_INTDEF_MAP.has(propertyPath)) { 222 translatedValue = this.getIntFlagsAsStrings(value, 223 this.FLICKER_INTDEF_MAP.get(propertyPath)); 224 } else { 225 // If it's a proto, search on the proto definition for the intdef type 226 const typeDefSpec = this.getTypeDefSpec(parentObj, propertyName); 227 if (typeDefSpec) { 228 translatedValue = this.getIntFlagsAsStrings(value, typeDefSpec); 229 } 230 } 231 232 return translatedValue; 233 } 234 235 /** 236 * Translate a property from its numerical value into its string representation 237 * 238 * @param intFlags Property value 239 * @param annotationType IntDef type to use 240 */ 241 private static getIntFlagsAsStrings(intFlags: any, annotationType: string) { 242 const flags = []; 243 244 const mapping = intDefMapping[annotationType].values; 245 const knownFlagValues = Object.keys(mapping).reverse().map(x => parseInt(x)); 246 247 if (mapping.length == 0) { 248 console.warn("No mapping for type", annotationType) 249 return intFlags + "" 250 } 251 252 // Will only contain bits that have not been associated with a flag. 253 const parsedIntFlags = parseInt(intFlags); 254 let leftOver = parsedIntFlags; 255 256 for (const flagValue of knownFlagValues) { 257 if (((leftOver & flagValue) && ((intFlags & flagValue) === flagValue)) 258 || (parsedIntFlags === 0 && flagValue === 0)) { 259 flags.push(mapping[flagValue]); 260 261 leftOver = leftOver & ~flagValue; 262 } 263 } 264 265 if (flags.length === 0) { 266 console.error('No valid flag mappings found for ', 267 intFlags, 'of type', annotationType); 268 } 269 270 if (leftOver) { 271 // If 0 is a valid flag value that isn't in the intDefMapping 272 // it will be ignored 273 flags.push(leftOver); 274 } 275 276 return flags.join(' | '); 277 } 278} 279