Presenter/Mediator changes to save presets.

Only for hierarchy based traces at the moment.

Screencast:https://screencast.googleplex.com/cast/NDY3MjEyMzU4NTk1Mzc5Mnw5ZjliNjQ2MC0zZA

Bug: 291222624
Test: npm run test:unit:ci

Change-Id: I4eafb48dcd99921db3c5b6f2a9ca75370405d92b
diff --git a/tools/winscope/src/app/mediator.ts b/tools/winscope/src/app/mediator.ts
index d39ad4b..33608fd 100644
--- a/tools/winscope/src/app/mediator.ts
+++ b/tools/winscope/src/app/mediator.ts
@@ -45,6 +45,7 @@
 import {TraceEntry} from 'trace/trace';
 import {TRACE_INFO} from 'trace/trace_info';
 import {TracePosition} from 'trace/trace_position';
+import {TraceType} from 'trace/trace_type';
 import {RequestedTraceTypes} from 'trace_collection/adb_files';
 import {View, Viewer, ViewType} from 'viewers/viewer';
 import {ViewerFactory} from 'viewers/viewer_factory';
@@ -275,6 +276,20 @@
         UserNotifier.add(new NoTraceTargetsSelected()).notify();
       },
     );
+
+    await event.visit(
+      WinscopeEventType.FILTER_PRESET_SAVE_REQUEST,
+      async (event) => {
+        await this.findViewerByType(event.traceType)?.onWinscopeEvent(event);
+      },
+    );
+
+    await event.visit(
+      WinscopeEventType.FILTER_PRESET_APPLY_REQUEST,
+      async (event) => {
+        await this.findViewerByType(event.traceType)?.onWinscopeEvent(event);
+      },
+    );
   }
 
   private async loadFiles(files: File[], source: FilesSource) {
@@ -519,4 +534,8 @@
       await overlay.onWinscopeEvent(event);
     }
   }
+
+  private findViewerByType(type: TraceType): Viewer | undefined {
+    return this.viewers.find((viewer) => viewer.getTraces()[0].type === type);
+  }
 }
diff --git a/tools/winscope/src/common/persistent_store_proxy.ts b/tools/winscope/src/common/persistent_store_proxy.ts
index 7c67552..af7087c 100644
--- a/tools/winscope/src/common/persistent_store_proxy.ts
+++ b/tools/winscope/src/common/persistent_store_proxy.ts
@@ -22,7 +22,7 @@
     defaultState: T,
     storage: Store,
   ): T {
-    const storedState = JSON.parse(storage.get(key) ?? '{}');
+    const storedState = JSON.parse(storage.get(key) ?? '{}', parseMap);
     const currentState = mergeDeep({}, structuredClone(defaultState));
     mergeDeepKeepingStructure(currentState, storedState);
     return wrapWithPersistentStoreProxy(key, currentState, storage) as T;
@@ -66,7 +66,7 @@
         (typeof prop === 'number' || !Number.isNaN(Number(prop)))
       ) {
         target[Number(prop)] = newValue;
-        storage.add(storeKey, JSON.stringify(baseObject));
+        storage.add(storeKey, JSON.stringify(baseObject, stringifyMap));
         return true;
       }
       if (!Array.isArray(target) && Array.isArray(newValue)) {
@@ -76,12 +76,12 @@
           storage,
           baseObject,
         );
-        storage.add(storeKey, JSON.stringify(baseObject));
+        storage.add(storeKey, JSON.stringify(baseObject, stringifyMap));
         return true;
       }
       if (!Array.isArray(target) && updatableProps.includes(prop)) {
         (target as any)[prop] = newValue;
-        storage.add(storeKey, JSON.stringify(baseObject));
+        storage.add(storeKey, JSON.stringify(baseObject, stringifyMap));
         return true;
       }
       throw new Error(
@@ -142,3 +142,20 @@
 
   return mergeDeep(target, ...sources);
 }
+
+export function stringifyMap(key: string, value: any) {
+  if (value instanceof Map) {
+    return {
+      type: 'Map',
+      value: [...value],
+    };
+  }
+  return value;
+}
+
+export function parseMap(key: string, value: any) {
+  if (value && value.type === 'Map') {
+    return new Map(value.value);
+  }
+  return value;
+}
diff --git a/tools/winscope/src/common/persistent_store_proxy_test.ts b/tools/winscope/src/common/persistent_store_proxy_test.ts
index 986807d..eaeb05d 100644
--- a/tools/winscope/src/common/persistent_store_proxy_test.ts
+++ b/tools/winscope/src/common/persistent_store_proxy_test.ts
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-import {InMemoryStorage} from 'common/in_memory_storage';
+import {InMemoryStorage} from './in_memory_storage';
 import {PersistentStoreProxy} from './persistent_store_proxy';
 
 describe('PersistentStoreObject', () => {
diff --git a/tools/winscope/src/viewers/common/abstract_hierarchy_viewer_presenter.ts b/tools/winscope/src/viewers/common/abstract_hierarchy_viewer_presenter.ts
index 1acca66..9cabd68 100644
--- a/tools/winscope/src/viewers/common/abstract_hierarchy_viewer_presenter.ts
+++ b/tools/winscope/src/viewers/common/abstract_hierarchy_viewer_presenter.ts
@@ -16,8 +16,13 @@
 
 import {assertDefined} from 'common/assert_utils';
 import {FunctionUtils} from 'common/function_utils';
+import {parseMap, stringifyMap} from 'common/persistent_store_proxy';
 import {Store} from 'common/store';
-import {TracePositionUpdate, WinscopeEvent} from 'messaging/winscope_event';
+import {
+  TracePositionUpdate,
+  WinscopeEvent,
+  WinscopeEventType,
+} from 'messaging/winscope_event';
 import {
   EmitEvent,
   WinscopeEventEmitter,
@@ -33,6 +38,7 @@
 import {UiHierarchyTreeNode} from 'viewers/common/ui_hierarchy_tree_node';
 import {UserOptions} from 'viewers/common/user_options';
 import {HierarchyPresenter} from './hierarchy_presenter';
+import {PresetHierarchy} from './preset_hierarchy';
 import {RectShowState} from './rect_show_state';
 import {TextFilter} from './text_filter';
 import {UiDataHierarchy} from './ui_data_hierarchy';
@@ -207,6 +213,58 @@
     this.copyUiDataAndNotifyView();
   }
 
+  protected async handleCommonWinscopeEvents(event: WinscopeEvent) {
+    await event.visit(
+      WinscopeEventType.FILTER_PRESET_SAVE_REQUEST,
+      async (event) => {
+        this.saveConfigAsPreset(event.name);
+      },
+    );
+  }
+
+  protected saveConfigAsPreset(storeKey: string) {
+    const preset: PresetHierarchy = {
+      hierarchyUserOptions: this.uiData.hierarchyUserOptions,
+      hierarchyFilter: this.uiData.hierarchyFilter,
+      propertiesUserOptions: this.uiData.propertiesUserOptions,
+      propertiesFilter: this.uiData.propertiesFilter,
+      rectsUserOptions: this.uiData.rectsUserOptions,
+      rectIdToShowState: this.uiData.rectIdToShowState,
+    };
+    this.storage.add(storeKey, JSON.stringify(preset, stringifyMap));
+  }
+
+  protected async applyPresetConfig(storeKey: string) {
+    const preset = this.storage.get(storeKey);
+    if (preset) {
+      const parsedPreset: PresetHierarchy = JSON.parse(preset, parseMap);
+      await this.hierarchyPresenter.applyHierarchyUserOptionsChange(
+        parsedPreset.hierarchyUserOptions,
+      );
+      await this.hierarchyPresenter.applyHierarchyFilterChange(
+        parsedPreset.hierarchyFilter,
+      );
+
+      this.propertiesPresenter.applyPropertiesUserOptionsChange(
+        parsedPreset.propertiesUserOptions,
+      );
+      this.propertiesPresenter.applyPropertiesFilterChange(
+        parsedPreset.propertiesFilter,
+      );
+      await this.updatePropertiesTree();
+
+      if (this.rectsPresenter) {
+        this.rectsPresenter?.applyRectsUserOptionsChange(
+          assertDefined(parsedPreset.rectsUserOptions),
+        );
+        this.rectsPresenter?.updateRectShowStates(
+          parsedPreset.rectIdToShowState,
+        );
+      }
+      this.refreshHierarchyViewerUiData();
+    }
+  }
+
   protected async applyTracePositionUpdate(event: TracePositionUpdate) {
     let entries: Array<TraceEntry<HierarchyTreeNode>> = [];
     if (this.multiTraceType !== undefined) {
diff --git a/tools/winscope/src/viewers/common/abstract_hierarchy_viewer_presenter_test.ts b/tools/winscope/src/viewers/common/abstract_hierarchy_viewer_presenter_test.ts
index 850bae8..f536c3b 100644
--- a/tools/winscope/src/viewers/common/abstract_hierarchy_viewer_presenter_test.ts
+++ b/tools/winscope/src/viewers/common/abstract_hierarchy_viewer_presenter_test.ts
@@ -16,10 +16,17 @@
 
 import {assertDefined} from 'common/assert_utils';
 import {Rect} from 'common/geometry/rect';
-import {TracePositionUpdate} from 'messaging/winscope_event';
+import {InMemoryStorage} from 'common/in_memory_storage';
+import {Store} from 'common/store';
+import {
+  FilterPresetApplyRequest,
+  FilterPresetSaveRequest,
+  TracePositionUpdate,
+} from 'messaging/winscope_event';
 import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils';
 import {TreeNodeUtils} from 'test/unit/tree_node_utils';
 import {UserNotifierChecker} from 'test/unit/user_notifier_checker';
+import {TraceType} from 'trace/trace_type';
 import {
   AbstractHierarchyViewerPresenter,
   NotifyHierarchyViewCallbackType,
@@ -41,6 +48,7 @@
       let uiData: UiDataHierarchy;
       let presenter: AbstractHierarchyViewerPresenter<UiData>;
       let userNotifierChecker: UserNotifierChecker;
+      let storage: InMemoryStorage;
 
       beforeAll(async () => {
         jasmine.addCustomEqualityTester(TreeNodeUtils.treeNodeEqualityTester);
@@ -49,9 +57,10 @@
       });
 
       beforeEach(() => {
+        storage = new InMemoryStorage();
         presenter = this.createPresenter((newData) => {
           uiData = newData;
-        });
+        }, storage);
       });
 
       afterEach(() => {
@@ -593,6 +602,42 @@
         });
       }
 
+      it('handles filter preset requests', async () => {
+        await presenter.onAppEvent(this.getPositionUpdate());
+        const saveEvent = new FilterPresetSaveRequest(
+          'TestPreset',
+          TraceType.TEST_TRACE_STRING,
+        );
+        expect(storage.get(saveEvent.name)).toBeUndefined();
+        await presenter.onAppEvent(saveEvent);
+        expect(storage.get(saveEvent.name)).toBeDefined();
+
+        await presenter.onHierarchyFilterChange(
+          new TextFilter('Test Filter', []),
+        );
+        await presenter.onHierarchyUserOptionsChange({});
+        await presenter.onPropertiesUserOptionsChange({});
+        await presenter.onPropertiesFilterChange(
+          new TextFilter('Test Filter', []),
+        );
+
+        if (this.shouldExecuteRectTests) {
+          presenter.onRectsUserOptionsChange({});
+          await presenter.onRectShowStateChange(
+            assertDefined(uiData.rectsToDraw)[0].id,
+            RectShowState.HIDE,
+          );
+        }
+        const currentUiData = uiData;
+
+        const applyEvent = new FilterPresetApplyRequest(
+          saveEvent.name,
+          TraceType.TEST_TRACE_STRING,
+        );
+        await presenter.onAppEvent(applyEvent);
+        expect(uiData).not.toEqual(currentUiData);
+      });
+
       function pinNode(node: UiHierarchyTreeNode) {
         presenter.onPinnedItemChange(node);
         expect(uiData.pinnedItems).toEqual([node]);
@@ -651,6 +696,7 @@
   abstract setUpTestEnvironment(): Promise<void>;
   abstract createPresenter(
     callback: NotifyHierarchyViewCallbackType<UiData>,
+    storage: Store,
   ): AbstractHierarchyViewerPresenter<UiData>;
   abstract createPresenterWithEmptyTrace(
     callback: NotifyHierarchyViewCallbackType<UiData>,
diff --git a/tools/winscope/src/viewers/common/abstract_presenter_input_method.ts b/tools/winscope/src/viewers/common/abstract_presenter_input_method.ts
index 59aac44..caa6503 100644
--- a/tools/winscope/src/viewers/common/abstract_presenter_input_method.ts
+++ b/tools/winscope/src/viewers/common/abstract_presenter_input_method.ts
@@ -136,6 +136,7 @@
   }
 
   async onAppEvent(event: WinscopeEvent) {
+    await this.handleCommonWinscopeEvents(event);
     await event.visit(
       WinscopeEventType.TRACE_POSITION_UPDATE,
       async (event) => {
@@ -187,6 +188,14 @@
         this.refreshUIData();
       },
     );
+    await event.visit(
+      WinscopeEventType.FILTER_PRESET_APPLY_REQUEST,
+      async (event) => {
+        const filterPresetName = event.name;
+        await this.applyPresetConfig(filterPresetName);
+        this.refreshUIData();
+      },
+    );
   }
 
   async onHighlightedNodeChange(node: UiHierarchyTreeNode) {
diff --git a/tools/winscope/src/viewers/common/abstract_presenter_input_method_test.ts b/tools/winscope/src/viewers/common/abstract_presenter_input_method_test.ts
index 99473aa..94b3d58 100644
--- a/tools/winscope/src/viewers/common/abstract_presenter_input_method_test.ts
+++ b/tools/winscope/src/viewers/common/abstract_presenter_input_method_test.ts
@@ -16,6 +16,7 @@
 
 import {assertDefined} from 'common/assert_utils';
 import {InMemoryStorage} from 'common/in_memory_storage';
+import {Store} from 'common/store';
 import {TracePositionUpdate} from 'messaging/winscope_event';
 import {TraceBuilder} from 'test/unit/trace_builder';
 import {TreeNodeUtils} from 'test/unit/tree_node_utils';
@@ -120,15 +121,11 @@
 
   override createPresenter(
     callback: NotifyHierarchyViewCallbackType<ImeUiData>,
+    storage: Store,
   ): AbstractPresenterInputMethod {
     const traces = assertDefined(this.traces);
     const trace = assertDefined(traces.getTrace(this.imeTraceType));
-    return new this.PresenterInputMethod(
-      trace,
-      traces,
-      new InMemoryStorage(),
-      callback,
-    );
+    return new this.PresenterInputMethod(trace, traces, storage, callback);
   }
 
   override getPositionUpdate(): TracePositionUpdate {
diff --git a/tools/winscope/src/viewers/common/preset_hierarchy.ts b/tools/winscope/src/viewers/common/preset_hierarchy.ts
new file mode 100644
index 0000000..6a9bffb
--- /dev/null
+++ b/tools/winscope/src/viewers/common/preset_hierarchy.ts
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2024 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 {UserOptions} from 'viewers/common/user_options';
+import {RectShowState} from './rect_show_state';
+import {TextFilter} from './text_filter';
+
+export interface PresetHierarchy {
+  hierarchyUserOptions: UserOptions;
+  hierarchyFilter: TextFilter;
+  propertiesUserOptions: UserOptions;
+  propertiesFilter: TextFilter;
+  rectsUserOptions?: UserOptions;
+  rectIdToShowState?: Map<string, RectShowState> | undefined;
+}
diff --git a/tools/winscope/src/viewers/viewer_surface_flinger/presenter.ts b/tools/winscope/src/viewers/viewer_surface_flinger/presenter.ts
index 2eed62c..f488825 100644
--- a/tools/winscope/src/viewers/viewer_surface_flinger/presenter.ts
+++ b/tools/winscope/src/viewers/viewer_surface_flinger/presenter.ts
@@ -185,6 +185,7 @@
   }
 
   override async onAppEvent(event: WinscopeEvent) {
+    await this.handleCommonWinscopeEvents(event);
     await event.visit(
       WinscopeEventType.TRACE_POSITION_UPDATE,
       async (event) => {
@@ -195,6 +196,15 @@
         this.refreshUIData();
       },
     );
+    await event.visit(
+      WinscopeEventType.FILTER_PRESET_APPLY_REQUEST,
+      async (event) => {
+        const filterPresetName = event.name;
+        await this.applyPresetConfig(filterPresetName);
+        this.updateCuratedProperties();
+        this.refreshUIData();
+      },
+    );
   }
 
   override async onHighlightedNodeChange(item: UiHierarchyTreeNode) {
diff --git a/tools/winscope/src/viewers/viewer_surface_flinger/presenter_test.ts b/tools/winscope/src/viewers/viewer_surface_flinger/presenter_test.ts
index 6b4b5ab..4438c75 100644
--- a/tools/winscope/src/viewers/viewer_surface_flinger/presenter_test.ts
+++ b/tools/winscope/src/viewers/viewer_surface_flinger/presenter_test.ts
@@ -17,6 +17,7 @@
 import {assertDefined} from 'common/assert_utils';
 import {Rect} from 'common/geometry/rect';
 import {InMemoryStorage} from 'common/in_memory_storage';
+import {Store} from 'common/store';
 import {TracePositionUpdate} from 'messaging/winscope_event';
 import {HierarchyTreeBuilder} from 'test/unit/hierarchy_tree_builder';
 import {TraceBuilder} from 'test/unit/trace_builder';
@@ -143,11 +144,12 @@
 
   override createPresenter(
     callback: NotifyHierarchyViewCallbackType<UiData>,
+    storage: Store,
   ): Presenter {
     const traces = new Traces();
     const traceSf = assertDefined(this.traceSf);
     traces.addTrace(traceSf);
-    return new Presenter(traceSf, traces, new InMemoryStorage(), callback);
+    return new Presenter(traceSf, traces, storage, callback);
   }
 
   override getPositionUpdate(): TracePositionUpdate {
@@ -286,6 +288,7 @@
         };
         presenter = this.createPresenter(
           notifyViewCallback as NotifyHierarchyViewCallbackType<UiData>,
+          new InMemoryStorage(),
         );
       });
 
diff --git a/tools/winscope/src/viewers/viewer_view_capture/presenter.ts b/tools/winscope/src/viewers/viewer_view_capture/presenter.ts
index 6cc56f0..14d30af 100644
--- a/tools/winscope/src/viewers/viewer_view_capture/presenter.ts
+++ b/tools/winscope/src/viewers/viewer_view_capture/presenter.ts
@@ -169,6 +169,7 @@
   }
 
   override async onAppEvent(event: WinscopeEvent) {
+    await this.handleCommonWinscopeEvents(event);
     await event.visit(
       WinscopeEventType.TRACE_POSITION_UPDATE,
       async (event) => {
@@ -192,6 +193,15 @@
         this.refreshUIData();
       },
     );
+    await event.visit(
+      WinscopeEventType.FILTER_PRESET_APPLY_REQUEST,
+      async (event) => {
+        const filterPresetName = event.name;
+        await this.applyPresetConfig(filterPresetName);
+        this.updateCuratedProperties();
+        this.refreshUIData();
+      },
+    );
   }
 
   override async onHighlightedNodeChange(node: UiHierarchyTreeNode) {
diff --git a/tools/winscope/src/viewers/viewer_view_capture/presenter_test.ts b/tools/winscope/src/viewers/viewer_view_capture/presenter_test.ts
index 34b0f53..6420306 100644
--- a/tools/winscope/src/viewers/viewer_view_capture/presenter_test.ts
+++ b/tools/winscope/src/viewers/viewer_view_capture/presenter_test.ts
@@ -17,6 +17,7 @@
 import {assertDefined} from 'common/assert_utils';
 import {Rect} from 'common/geometry/rect';
 import {InMemoryStorage} from 'common/in_memory_storage';
+import {Store} from 'common/store';
 import {TracePositionUpdate} from 'messaging/winscope_event';
 import {TraceBuilder} from 'test/unit/trace_builder';
 import {UnitTestUtils} from 'test/unit/utils';
@@ -122,12 +123,9 @@
 
   override createPresenter(
     callback: NotifyHierarchyViewCallbackType<UiData>,
+    storage: Store,
   ): Presenter {
-    return new Presenter(
-      assertDefined(this.traces),
-      new InMemoryStorage(),
-      callback,
-    );
+    return new Presenter(assertDefined(this.traces), storage, callback);
   }
 
   override getPositionUpdate(): TracePositionUpdate {
diff --git a/tools/winscope/src/viewers/viewer_window_manager/presenter.ts b/tools/winscope/src/viewers/viewer_window_manager/presenter.ts
index dc93169..1ffd4df 100644
--- a/tools/winscope/src/viewers/viewer_window_manager/presenter.ts
+++ b/tools/winscope/src/viewers/viewer_window_manager/presenter.ts
@@ -143,6 +143,7 @@
   }
 
   override async onAppEvent(event: WinscopeEvent) {
+    await this.handleCommonWinscopeEvents(event);
     await event.visit(
       WinscopeEventType.TRACE_POSITION_UPDATE,
       async (event) => {
@@ -150,6 +151,14 @@
         this.refreshUIData();
       },
     );
+    await event.visit(
+      WinscopeEventType.FILTER_PRESET_APPLY_REQUEST,
+      async (event) => {
+        const filterPresetName = event.name;
+        await this.applyPresetConfig(filterPresetName);
+        this.refreshUIData();
+      },
+    );
   }
 
   override async onHighlightedNodeChange(item: UiHierarchyTreeNode) {
diff --git a/tools/winscope/src/viewers/viewer_window_manager/presenter_test.ts b/tools/winscope/src/viewers/viewer_window_manager/presenter_test.ts
index 663d860..1168e18 100644
--- a/tools/winscope/src/viewers/viewer_window_manager/presenter_test.ts
+++ b/tools/winscope/src/viewers/viewer_window_manager/presenter_test.ts
@@ -17,6 +17,7 @@
 import {assertDefined} from 'common/assert_utils';
 import {Rect} from 'common/geometry/rect';
 import {InMemoryStorage} from 'common/in_memory_storage';
+import {Store} from 'common/store';
 import {TracePositionUpdate} from 'messaging/winscope_event';
 import {TraceBuilder} from 'test/unit/trace_builder';
 import {UnitTestUtils} from 'test/unit/utils';
@@ -108,11 +109,12 @@
 
   override createPresenter(
     callback: NotifyHierarchyViewCallbackType<UiData>,
+    storage: Store,
   ): Presenter {
     const traces = new Traces();
     const trace = assertDefined(this.trace);
     traces.addTrace(trace);
-    return new Presenter(trace, traces, new InMemoryStorage(), callback);
+    return new Presenter(trace, traces, storage, callback);
   }
 
   override getPositionUpdate(): TracePositionUpdate {