• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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