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