1/*
2 * Copyright (C) 2023 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 {assertDefined} from 'common/assert_utils';
18import {FunctionUtils} from 'common/function_utils';
19import {Timestamp} from 'common/time';
20import {
21  TracePositionUpdate,
22  WinscopeEvent,
23  WinscopeEventType,
24} from 'messaging/winscope_event';
25import {
26  EmitEvent,
27  WinscopeEventEmitter,
28} from 'messaging/winscope_event_emitter';
29import {CustomQueryType} from 'trace/custom_query';
30import {AbsoluteEntryIndex, Trace} from 'trace/trace';
31import {Traces} from 'trace/traces';
32import {TraceEntryFinder} from 'trace/trace_entry_finder';
33import {TraceType} from 'trace/trace_type';
34import {Transition} from 'trace/transition';
35import {HierarchyTreeNode} from 'trace/tree_node/hierarchy_tree_node';
36import {PropertyTreeNode} from 'trace/tree_node/property_tree_node';
37import {Filter} from 'viewers/common/operations/filter';
38import {UiPropertyTreeNode} from 'viewers/common/ui_property_tree_node';
39import {UiTreeFormatter} from 'viewers/common/ui_tree_formatter';
40import {UiTreeUtils} from 'viewers/common/ui_tree_utils';
41import {UpdateTransitionChangesNames} from './operations/update_transition_changes_names';
42import {UiData} from './ui_data';
43
44export class Presenter implements WinscopeEventEmitter {
45  private isInitialized = false;
46  private transitionTrace: Trace<PropertyTreeNode>;
47  private surfaceFlingerTrace: Trace<HierarchyTreeNode> | undefined;
48  private windowManagerTrace: Trace<HierarchyTreeNode> | undefined;
49  private layerIdToName = new Map<number, string>();
50  private windowTokenToTitle = new Map<string, string>();
51  private uiData = UiData.EMPTY;
52  private readonly notifyUiDataCallback: (data: UiData) => void;
53  private emitAppEvent: EmitEvent = FunctionUtils.DO_NOTHING_ASYNC;
54
55  constructor(
56    trace: Trace<PropertyTreeNode>,
57    traces: Traces,
58    notifyUiDataCallback: (data: UiData) => void,
59  ) {
60    this.transitionTrace = trace;
61    this.surfaceFlingerTrace = traces.getTrace(TraceType.SURFACE_FLINGER);
62    this.windowManagerTrace = traces.getTrace(TraceType.WINDOW_MANAGER);
63    this.notifyUiDataCallback = notifyUiDataCallback;
64  }
65
66  setEmitEvent(callback: EmitEvent) {
67    this.emitAppEvent = callback;
68  }
69
70  async onAppEvent(event: WinscopeEvent) {
71    await event.visit(
72      WinscopeEventType.TRACE_POSITION_UPDATE,
73      async (event) => {
74        await this.initializeIfNeeded();
75
76        if (this.uiData === UiData.EMPTY) {
77          this.uiData = await this.computeUiData();
78        }
79
80        const entry = TraceEntryFinder.findCorrespondingEntry(
81          this.transitionTrace,
82          event.position,
83        );
84
85        const transition = await entry?.getValue();
86        if (transition !== undefined) {
87          this.onTransitionSelected(transition);
88        }
89
90        this.notifyUiDataCallback(this.uiData);
91      },
92    );
93  }
94
95  onTransitionSelected(transition: PropertyTreeNode): void {
96    this.uiData.selectedTransition = this.makeUiPropertiesTree(transition);
97    this.notifyUiDataCallback(this.uiData);
98  }
99
100  async onRawTimestampClicked(timestamp: Timestamp) {
101    await this.emitAppEvent(TracePositionUpdate.fromTimestamp(timestamp, true));
102  }
103
104  async onLogTimestampClicked(traceIndex: AbsoluteEntryIndex) {
105    await this.emitAppEvent(
106      TracePositionUpdate.fromTraceEntry(
107        this.transitionTrace.getEntry(traceIndex),
108        true,
109      ),
110    );
111  }
112
113  private async initializeIfNeeded() {
114    if (this.isInitialized) {
115      return;
116    }
117
118    if (this.surfaceFlingerTrace) {
119      const layersIdAndName = await this.surfaceFlingerTrace.customQuery(
120        CustomQueryType.SF_LAYERS_ID_AND_NAME,
121      );
122      layersIdAndName.forEach((value) => {
123        this.layerIdToName.set(value.id, value.name);
124      });
125    }
126
127    if (this.windowManagerTrace) {
128      const windowsTokenAndTitle = await this.windowManagerTrace.customQuery(
129        CustomQueryType.WM_WINDOWS_TOKEN_AND_TITLE,
130      );
131      windowsTokenAndTitle.forEach((value) => {
132        this.windowTokenToTitle.set(value.token, value.title);
133      });
134    }
135
136    this.isInitialized = true;
137  }
138
139  private async computeUiData(): Promise<UiData> {
140    const entryPromises = this.transitionTrace.mapEntry((entry) => {
141      return entry.getValue();
142    });
143    const entries = await Promise.all(entryPromises);
144    // TODO(b/339191691): Ideally we should refactor the parsers to
145    // keep a map of time -> rowId, instead of relying on table order
146    const transitions = this.makeTransitions(entries);
147    this.sortTransitions(transitions);
148    const selectedTransition = this.uiData?.selectedTransition ?? undefined;
149
150    return new UiData(transitions, selectedTransition);
151  }
152
153  private sortTransitions(transitions: Transition[]) {
154    transitions.sort((a: Transition, b: Transition) => {
155      return a.id <= b.id ? -1 : 1;
156    });
157  }
158
159  private makeTransitions(entries: PropertyTreeNode[]): Transition[] {
160    return entries.map((transitionNode, index) => {
161      const wmDataNode = assertDefined(transitionNode.getChildByName('wmData'));
162      const shellDataNode = assertDefined(
163        transitionNode.getChildByName('shellData'),
164      );
165
166      const transition: Transition = {
167        id: assertDefined(transitionNode.getChildByName('id')).getValue(),
168        type: wmDataNode.getChildByName('type')?.formattedValue() ?? 'NONE',
169        sendTime: wmDataNode.getChildByName('sendTimeNs'),
170        dispatchTime: shellDataNode.getChildByName('dispatchTimeNs'),
171        duration: transitionNode.getChildByName('duration')?.formattedValue(),
172        merged: assertDefined(
173          transitionNode.getChildByName('merged'),
174        ).getValue(),
175        aborted: assertDefined(
176          transitionNode.getChildByName('aborted'),
177        ).getValue(),
178        played: assertDefined(
179          transitionNode.getChildByName('played'),
180        ).getValue(),
181        propertiesTree: transitionNode,
182        traceIndex: index,
183      };
184      return transition;
185    });
186  }
187
188  private makeUiPropertiesTree(
189    transitionNode: PropertyTreeNode,
190  ): UiPropertyTreeNode {
191    const tree = UiPropertyTreeNode.from(transitionNode);
192
193    return new UiTreeFormatter<UiPropertyTreeNode>()
194      .setUiTree(tree)
195      .addOperation(new Filter([UiTreeUtils.isNotCalculated], false))
196      .addOperation(
197        new UpdateTransitionChangesNames(
198          this.layerIdToName,
199          this.windowTokenToTitle,
200        ),
201      )
202      .format();
203  }
204}
205