/* * Copyright 2021, The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import {toSize, toBuffer, toColor, toPoint, toRect, toRectF, toRegion, toTransform} from './common'; import intDefMapping from '../../../../../prebuilts/misc/common/winscope/intDefMapping.json'; import config from '../config/Configuration.json' function readIntdefMap(): Map { const map = new Map(); const keys = Object.keys(config.intDefColumn); keys.forEach(key => { const value = config.intDefColumn[key]; map.set(key, value); }); return map; } export default class ObjectFormatter { static displayDefaults: boolean = false private static INVALID_ELEMENT_PROPERTIES = config.invalidProperties; private static FLICKER_INTDEF_MAP = readIntdefMap(); static cloneObject(entry: any): any { let obj: any = {} const properties = ObjectFormatter.getProperties(entry); properties.forEach(prop => obj[prop] = entry[prop]); return obj; } /** * Get the true properties of an entry excluding functions, kotlin gernerated * variables, explicitly excluded properties, and flicker objects already in * the hierarchy that shouldn't be traversed when formatting the entry * @param entry The entry for which we want to get the properties for * @return The "true" properties of the entry as described above */ static getProperties(entry: any): string[] { var props = []; let obj = entry; do { const properties = Object.getOwnPropertyNames(obj).filter(it => { // filter out functions if (typeof(entry[it]) === 'function') return false; // internal propertires from kotlinJs if (it.includes(`$`)) return false; // private kotlin variables from kotlin if (it.startsWith(`_`)) return false; // some predefined properties used only internally (e.g., children, ref, diff) if (this.INVALID_ELEMENT_PROPERTIES.includes(it)) return false; const value = entry[it]; // only non-empty arrays of non-flicker objects (otherwise they are in hierarchy) if (Array.isArray(value) && value.length > 0) return !value[0].stableId; // non-flicker object return !(value?.stableId); }); properties.forEach(function (prop) { if (typeof(entry[prop]) !== 'function' && props.indexOf(prop) === -1) { props.push(prop); } }); } while (obj = Object.getPrototypeOf(obj)); return props; } /** * Format a Winscope entry to be displayed in the UI * Accounts for different user display settings (e.g. hiding empty/default values) * @param obj The raw object to format * @return The formatted object */ static format(obj: any): {} { const properties = this.getProperties(obj); const sortedProperties = properties.sort() const result: any = {} sortedProperties.forEach(entry => { const key = entry; const value: any = obj[key]; if (value === null || value === undefined) { if (this.displayDefaults) { result[key] = value } return } if (value || this.displayDefaults) { // flicker obj if (value.prettyPrint) { const isEmpty = value.isEmpty === true; if (!isEmpty || this.displayDefaults) { result[key] = value.prettyPrint() } } else { // converted proto to flicker const translatedObject = this.translateObject(value) if (translatedObject) { result[key] = translatedObject.prettyPrint() // objects - recursive call } else if (value && typeof(value) == `object`) { const childObj = this.format(value) as any const isEmpty = Object.entries(childObj).length == 0 || childObj.isEmpty if (!isEmpty || this.displayDefaults) { result[key] = childObj } } else { // values result[key] = this.translateIntDef(obj, key, value) } } } }) // return Object.freeze(result) return result } /** * Translate some predetermined proto objects into their flicker equivalent * * Returns null if the object cannot be translated * * @param obj Object to translate */ private static translateObject(obj) { const type = obj?.$type?.name switch(type) { case `SizeProto`: return toSize(obj) case `ActiveBufferProto`: return toBuffer(obj) case `ColorProto`: return toColor(obj) case `PointProto`: return toPoint(obj) case `RectProto`: return toRect(obj) case `FloatRectProto`: return toRectF(obj) case `RegionProto`: return toRegion(obj) case `TransformProto`: return toTransform(obj) case 'ColorTransformProto': { const formatted = this.formatColorTransform(obj.val); return `${formatted}`; } } return null } private static formatColorTransform(vals) { const fixedVals = vals.map((v) => v.toFixed(1)); let formatted = ``; for (let i = 0; i < fixedVals.length; i += 4) { formatted += `[`; formatted += fixedVals.slice(i, i + 4).join(', '); formatted += `] `; } return formatted; } /** * Obtains from the proto field, the metadata related to the typedef type (if any) * * @param obj Proto object * @param propertyName Property to search */ private static getTypeDefSpec(obj: any, propertyName: string): string { const fields = obj?.$type?.fields if (!fields) { return null } const options = fields[propertyName]?.options if (!options) { return null } return options["(.android.typedef)"] } /** * Translate intdef properties into their string representation * * For proto objects check the * * @param parentObj Object containing the value to parse * @param propertyName Property to search * @param value Property value */ private static translateIntDef(parentObj: any, propertyName: string, value: any): string { const parentClassName = parentObj.constructor.name const propertyPath = `${parentClassName}.${propertyName}` let translatedValue = value // Parse Flicker objects (no intdef annotation supported) if (this.FLICKER_INTDEF_MAP.has(propertyPath)) { translatedValue = this.getIntFlagsAsStrings(value, this.FLICKER_INTDEF_MAP.get(propertyPath)) } else { // If it's a proto, search on the proto definition for the intdef type const typeDefSpec = this.getTypeDefSpec(parentObj, propertyName) if (typeDefSpec) { translatedValue = this.getIntFlagsAsStrings(value, typeDefSpec) } } return translatedValue } /** * Translate a property from its numerical value into its string representation * * @param intFlags Property value * @param annotationType IntDef type to use */ private static getIntFlagsAsStrings(intFlags: any, annotationType: string) { const flags = []; const mapping = intDefMapping[annotationType].values; const knownFlagValues = Object.keys(mapping).reverse().map(x => parseInt(x)); if (mapping.length == 0) { console.warn("No mapping for type", annotationType) return intFlags + "" } // Will only contain bits that have not been associated with a flag. const parsedIntFlags = parseInt(intFlags); let leftOver = parsedIntFlags; for (const flagValue of knownFlagValues) { if (((leftOver & flagValue) && ((intFlags & flagValue) === flagValue)) || (parsedIntFlags === 0 && flagValue === 0)) { flags.push(mapping[flagValue]); leftOver = leftOver & ~flagValue; } } if (flags.length === 0) { console.error('No valid flag mappings found for ', intFlags, 'of type', annotationType); } if (leftOver) { // If 0 is a valid flag value that isn't in the intDefMapping // it will be ignored flags.push(leftOver); } return flags.join(' | '); } }