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