| /* |
| * 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 {toBounds, toBuffer, toColor, toPoint, toRect, |
| toRectF, toRegion, toTransform} from './common'; |
| import intDefMapping from |
| '../../../../../prebuilts/misc/common/winscope/intDefMapping.json'; |
| |
| export default class ObjectFormatter { |
| private static INVALID_ELEMENT_PROPERTIES = ['length', 'name', 'prototype', 'children', |
| 'childrenWindows', 'ref', 'root', 'layers', 'resolvedChildren'] |
| |
| private static FLICKER_INTDEF_MAP = new Map([ |
| [`WindowLayoutParams.type`, `android.view.WindowManager.LayoutParams.WindowType`], |
| [`WindowLayoutParams.flags`, `android.view.WindowManager.LayoutParams.Flags`], |
| [`WindowLayoutParams.privateFlags`, `android.view.WindowManager.LayoutParams.PrivateFlags`], |
| [`WindowLayoutParams.gravity`, `android.view.Gravity.GravityFlags`], |
| [`WindowLayoutParams.softInputMode`, `android.view.WindowManager.LayoutParams.WindowType`], |
| [`WindowLayoutParams.systemUiVisibilityFlags`, `android.view.WindowManager.LayoutParams.SystemUiVisibilityFlags`], |
| [`WindowLayoutParams.subtreeSystemUiVisibilityFlags`, `android.view.WindowManager.LayoutParams.SystemUiVisibilityFlags`], |
| [`WindowLayoutParams.behavior`, `android.view.WindowInsetsController.Behavior`], |
| [`WindowLayoutParams.fitInsetsSides`, `android.view.WindowInsets.Side.InsetsSide`], |
| |
| [`Configuration.windowingMode`, `android.app.WindowConfiguration.WindowingMode`], |
| [`WindowConfiguration.windowingMode`, `android.app.WindowConfiguration.WindowingMode`], |
| [`Configuration.orientation`, `android.content.pm.ActivityInfo.ScreenOrientation`], |
| [`WindowConfiguration.orientation`, `android.content.pm.ActivityInfo.ScreenOrientation`], |
| [`WindowState.orientation`, `android.content.pm.ActivityInfo.ScreenOrientation`], |
| ]) |
| |
| static format(obj: any): {} { |
| const entries = Object.entries(obj) |
| .filter(it => !it[0].includes(`$`)) |
| .filter(it => !this.INVALID_ELEMENT_PROPERTIES.includes(it[0])) |
| const sortedEntries = entries.sort() |
| |
| const result: any = {} |
| sortedEntries.forEach(entry => { |
| const key = entry[0] |
| const value: any = entry[1] |
| |
| if (value) { |
| // flicker obj |
| if (value.prettyPrint) { |
| 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`) { |
| result[key] = this.format(value) |
| } else { |
| // values |
| result[key] = this.translateIntDef(obj, key, value) |
| } |
| } |
| |
| } |
| }) |
| |
| return Object.freeze(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 toBounds(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(' | '); |
| } |
| } |