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