xref: /aosp_15_r20/development/tools/winscope/src/app/components/snack_bar_opener.ts (revision 90c8c64db3049935a07c6143d7fd006e26f8ecca)
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 {Inject, Injectable, NgZone} from '@angular/core';
18import {MatSnackBar} from '@angular/material/snack-bar';
19import {assertDefined} from 'common/assert_utils';
20import {NotificationType, UserNotification} from 'messaging/user_notification';
21import {SnackBarComponent} from './snack_bar_component';
22
23type Messages = string[];
24
25@Injectable({providedIn: 'root'})
26export class SnackBarOpener {
27  private static CROP_THRESHOLD = 5;
28  private isOpen = false;
29  private queue: Messages[] = [];
30
31  constructor(
32    @Inject(NgZone) private ngZone: NgZone,
33    @Inject(MatSnackBar) private snackBar: MatSnackBar,
34  ) {}
35
36  onNotifications(notifications: UserNotification[]) {
37    const messages = this.convertNotificationsToMessages(notifications);
38
39    if (messages.length === 0) {
40      return;
41    }
42
43    if (this.isOpen) {
44      this.queue.push(messages);
45      return;
46    }
47
48    this.displayMessages(messages);
49  }
50
51  private convertNotificationsToMessages(
52    notifications: UserNotification[],
53  ): Messages {
54    const messages: Messages = [];
55
56    const warnings = notifications.filter(
57      (n) => n.getNotificationType() === NotificationType.WARNING,
58    );
59    const groups = this.groupNotificationsByDescriptor(warnings);
60
61    for (const groupedWarnings of groups) {
62      const countUsed = Math.min(
63        groupedWarnings.length,
64        SnackBarOpener.CROP_THRESHOLD,
65      );
66      const countCropped = groupedWarnings.length - countUsed;
67
68      groupedWarnings.slice(0, countUsed).forEach((warning) => {
69        messages.push(warning.getMessage());
70      });
71
72      if (countCropped > 0) {
73        messages.push(
74          `... (cropped ${countCropped} '${groupedWarnings[0].getDescriptor()}' message${
75            countCropped > 1 ? 's' : ''
76          })`,
77        );
78      }
79    }
80
81    return messages;
82  }
83
84  private groupNotificationsByDescriptor(
85    warnings: UserNotification[],
86  ): Set<UserNotification[]> {
87    const groups = new Map<string, UserNotification[]>();
88
89    warnings.forEach((warning) => {
90      if (groups.get(warning.getDescriptor()) === undefined) {
91        groups.set(warning.getDescriptor(), []);
92      }
93      assertDefined(groups.get(warning.getDescriptor())).push(warning);
94    });
95
96    return new Set(groups.values());
97  }
98
99  private displayMessages(messages: Messages) {
100    this.ngZone.run(() => {
101      // The snackbar needs to be opened within ngZone,
102      // otherwise it will first display on the left and then will jump to the center
103      this.isOpen = true;
104      const ref = this.snackBar.openFromComponent(SnackBarComponent, {
105        data: messages,
106        duration: 5000 * messages.length,
107      });
108      ref.afterDismissed().subscribe(() => {
109        this.isOpen = false;
110        const next = this.queue.shift();
111        if (next !== undefined) {
112          this.displayMessages(next);
113        }
114      });
115    });
116  }
117}
118