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