1 /*
2  * Copyright (C) 2018 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 package com.android.car.notification;
17 
18 import android.annotation.NonNull;
19 import android.annotation.Nullable;
20 import android.app.Notification;
21 import android.os.Build;
22 import android.os.Bundle;
23 
24 import java.util.ArrayList;
25 import java.util.List;
26 
27 /**
28  * Data structure representing a notification card in car.
29  * A notification group can hold either:
30  * <ol>
31  * <li>One notification with no group summary notification</li>
32  * <li>One group summary notification with no child notifications</li>
33  * <li>A group of notifications with a group summary notification</li>
34  * </ol>
35  */
36 public class NotificationGroup {
37     private static final String TAG = "NotificationGroup";
38     private static final boolean DEBUG = Build.IS_DEBUGGABLE;
39 
40     private final List<AlertEntry> mNotifications = new ArrayList<>();
41 
42     @Nullable
43     private List<String> mChildTitles;
44     @Nullable
45     private AlertEntry mGroupSummaryNotification;
46     private String mGroupKey;
47     private boolean mIsHeader;
48     private boolean mIsFooter;
49     private boolean mIsRecentsHeader;
50     private boolean mIsOlderHeader;
51     private boolean mIsSeen;
52 
NotificationGroup()53     public NotificationGroup() {
54     }
55 
NotificationGroup(AlertEntry alertEntry)56     public NotificationGroup(AlertEntry alertEntry) {
57         addNotification(alertEntry);
58     }
59 
NotificationGroup(NotificationGroup group)60     public NotificationGroup(NotificationGroup group) {
61         setGroupKey(group.getGroupKey());
62         if (group.getGroupSummaryNotification() != null) {
63             setGroupSummaryNotification(group.getGroupSummaryNotification());
64         }
65         for (AlertEntry alertEntry : group.getChildNotifications()) {
66             addNotification(alertEntry);
67         }
68         setChildTitles(group.getChildTitles());
69         setFooter(group.isFooter());
70         setHeader(group.isHeader());
71         setOlderHeader(group.isOlderHeader());
72         setRecentsHeader(group.isRecentsHeader());
73         setSeen(group.isSeen());
74     }
75 
76     /**
77      * Add child notification.
78      *
79      * New notification must have the same group key as other notifications in group.
80      */
addNotification(AlertEntry alertEntry)81     public void addNotification(AlertEntry alertEntry) {
82         assertSameGroupKey(alertEntry.getStatusBarNotification().getGroupKey());
83         mNotifications.add(alertEntry);
84     }
85 
86     /**
87      * Removes child notification.
88      *
89      * @return {@code true} if notification was removed
90      */
removeNotification(AlertEntry alertEntry)91     public boolean removeNotification(AlertEntry alertEntry) {
92         for (int i = 0; i < mNotifications.size(); i++) {
93             if (mNotifications.get(i).getKey().equals(alertEntry.getKey())) {
94                 mNotifications.remove(i);
95                 return true;
96             }
97         }
98         return false;
99     }
100 
101     /**
102      * Set group summary notification.
103      *
104      * Group summary must have the same group key as other notifications in group.
105      */
setGroupSummaryNotification(AlertEntry groupSummaryNotification)106     public void setGroupSummaryNotification(AlertEntry groupSummaryNotification) {
107         assertSameGroupKey(groupSummaryNotification.getStatusBarNotification().getGroupKey());
108         mGroupSummaryNotification = groupSummaryNotification;
109     }
110 
setGroupKey(@onNull String groupKey)111     void setGroupKey(@NonNull String groupKey) {
112         mGroupKey = groupKey;
113     }
114 
115     /**
116      * Returns the group key of this notification group.
117      *
118      * <p> {@code null} will be returned if the group key has not been set yet.
119      */
120     @Nullable
getGroupKey()121     public String getGroupKey() {
122         return mGroupKey;
123     }
124 
125     /**
126      * Returns the count of how many child notifications (excluding the group summary notification)
127      * this notification group has.
128      */
getChildCount()129     public int getChildCount() {
130         return mNotifications.size();
131     }
132 
133     /**
134      * Returns true when it has a group summary notification and >1 child notifications
135      */
isGroup()136     public boolean isGroup() {
137         return mGroupSummaryNotification != null && getChildCount() > 1;
138     }
139 
140     /**
141      * Return true if this group is a header, footer, recents header or older header.
142      */
isHeaderOrFooter()143     public boolean isHeaderOrFooter() {
144         return isHeader() || isFooter() || isOlderHeader() || isRecentsHeader();
145     }
146 
147     /**
148      * Return true if the header is set to be displayed.
149      */
isHeader()150     public boolean isHeader() {
151         return mIsHeader;
152     }
153 
154     /**
155      * Set this to true if a header needs to be displayed with a title and a clear all button.
156      */
setHeader(boolean header)157     public void setHeader(boolean header) {
158         mIsHeader = header;
159     }
160 
161     /**
162      * Return true if the header is set to be displayed.
163      */
isFooter()164     public boolean isFooter() {
165         return mIsFooter;
166     }
167 
168     /**
169      * Set this to true if a footer needs to be displayed with a clear all button.
170      */
setFooter(boolean footer)171     public void setFooter(boolean footer) {
172         mIsFooter = footer;
173     }
174 
175     /**
176      * Return true if the recents header is set to be displayed.
177      */
isRecentsHeader()178     public boolean isRecentsHeader() {
179         return mIsRecentsHeader;
180     }
181 
182     /**
183      * Set this to true if a header is a recents header.
184      */
setRecentsHeader(boolean isRecentsHeader)185     public void setRecentsHeader(boolean isRecentsHeader) {
186         mIsRecentsHeader = isRecentsHeader;
187     }
188 
189     /**
190      * Return true if the older notifications header is set to be displayed.
191      */
isOlderHeader()192     public boolean isOlderHeader() {
193         return mIsOlderHeader;
194     }
195 
196     /**
197      * Set this to true if a header is a older notifications header.
198      */
setOlderHeader(boolean isOlderHeader)199     public void setOlderHeader(boolean isOlderHeader) {
200         mIsOlderHeader = isOlderHeader;
201     }
202 
203     /**
204      * Return true if the notification group has been seen.
205      */
isSeen()206     public boolean isSeen() {
207         return mIsSeen;
208     }
209 
210     /**
211      * Set this to true if the notification group has been seen.
212      */
setSeen(boolean isSeen)213     public void setSeen(boolean isSeen) {
214         mIsSeen = isSeen;
215     }
216 
217     /**
218      * Returns true if this group is not a header or footer and all of the notifications it holds
219      * are dismissible by user action.
220      */
isDismissible()221     public boolean isDismissible() {
222         if (mIsHeader || mIsFooter) {
223             return false;
224         }
225 
226         for (AlertEntry notification : mNotifications) {
227             boolean isForeground =
228                     (notification.getNotification().flags & Notification.FLAG_FOREGROUND_SERVICE)
229                             != 0;
230             if (isForeground || notification.getStatusBarNotification().isOngoing()) {
231                 return false;
232             }
233         }
234         return true;
235     }
236 
237     /**
238      * Returns the list of the child notifications.
239      */
getChildNotifications()240     public List<AlertEntry> getChildNotifications() {
241         return mNotifications;
242     }
243 
244     /**
245      * Returns the group summary notification.
246      */
247     @Nullable
getGroupSummaryNotification()248     public AlertEntry getGroupSummaryNotification() {
249         return mGroupSummaryNotification;
250     }
251 
252     /**
253      * Sets the list of child notification titles.
254      */
setChildTitles(List<String> childTitles)255     public void setChildTitles(List<String> childTitles) {
256         mChildTitles = childTitles;
257     }
258 
259     /**
260      * Returns the list of child notification titles.
261      */
262     @Nullable
getChildTitles()263     public List<String> getChildTitles() {
264         return mChildTitles;
265     }
266 
267     /**
268      * Generates the list of the child notification titles for a group summary notification.
269      */
generateChildTitles()270     public List<String> generateChildTitles() {
271         List<String> titles = new ArrayList<>();
272 
273         for (AlertEntry notification : mNotifications) {
274             Bundle extras = notification.getNotification().extras;
275             if (extras.containsKey(Notification.EXTRA_TITLE)) {
276                 titles.add(extras.getString(Notification.EXTRA_TITLE));
277             } else if (extras.containsKey(Notification.EXTRA_TITLE_BIG)) {
278                 titles.add(extras.getString(Notification.EXTRA_TITLE_BIG));
279             } else if (extras.containsKey(Notification.EXTRA_MESSAGES)) {
280                 List<Notification.MessagingStyle.Message> messages =
281                         Notification.MessagingStyle.Message.getMessagesFromBundleArray(
282                                 extras.getParcelableArray(Notification.EXTRA_MESSAGES));
283                 Notification.MessagingStyle.Message lastMessage = messages.get(messages.size() - 1);
284                 titles.add(lastMessage.getSenderPerson().getName().toString());
285             } else if (extras.containsKey(Notification.EXTRA_SUB_TEXT)) {
286                 titles.add(extras.getString(Notification.EXTRA_SUB_TEXT));
287             }
288         }
289 
290         return titles;
291     }
292 
293     /**
294      * Returns a single notification that represents this NotificationGroup:
295      *
296      * <p> If the NotificationGroup is a valid grouped notification or has no child notifications,
297      * the group summary notification is returned.
298      *
299      * <p> If the NotificationGroup has only 1 child notification,
300      * or has more than 1 child notifications without a valid group summary,
301      * the first child notification is returned.
302      *
303      * @return the notification that represents this NotificationGroup
304      */
getSingleNotification()305     public AlertEntry getSingleNotification() {
306         if (isGroup() || getChildCount() == 0) {
307             return getGroupSummaryNotification();
308         } else {
309             return mNotifications.get(0);
310         }
311     }
312 
getNotificationForSorting()313     AlertEntry getNotificationForSorting() {
314         if (mGroupSummaryNotification != null) {
315             return getGroupSummaryNotification();
316         }
317         return getSingleNotification();
318     }
319 
assertSameGroupKey(String groupKey)320     private void assertSameGroupKey(String groupKey) {
321         if (mGroupKey == null) {
322             setGroupKey(groupKey);
323         } else if (!mGroupKey.equals(groupKey)) {
324             throw new IllegalStateException(
325                     "Group key mismatch when adding a notification to a group. " +
326                             "mGroupKey: " + mGroupKey + "; groupKey:" + groupKey);
327         }
328     }
329 
330     @Override
toString()331     public String toString() {
332         return mGroupKey + ": " + mNotifications.toString();
333     }
334 }
335