blob: b67bfe60861f9fe0070d88d35d95ed89444a7cd4 [file] [log] [blame]
/*
* 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<string, string> {
const map = new Map<string, string>();
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;
}
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;
// Flicker object properties or arrays
if (!entry[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;
}
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 || (this.displayDefaults && value !== undefined && value !== null)) {
// 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(' | ');
}
}