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 a notification with matching key or else returns {@code null}. 248 */ getChildNotification(String key)249 public AlertEntry getChildNotification(String key) { 250 for (int i = 0; i < mNotifications.size(); i++) { 251 AlertEntry alertEntry = mNotifications.get(i); 252 if (alertEntry.getKey().equals(key)) { 253 return alertEntry; 254 } 255 } 256 return null; 257 } 258 259 /** 260 * Returns {@code true} if old notification is set to new notification. 261 */ updateNotification(AlertEntry oldValue, AlertEntry newValue)262 public boolean updateNotification(AlertEntry oldValue, AlertEntry newValue) { 263 for (int i = 0; i < mNotifications.size(); i++) { 264 AlertEntry alertEntry = mNotifications.get(i); 265 if (alertEntry.getKey().equals(oldValue.getKey())) { 266 mNotifications.set(i, newValue); 267 return true; 268 } 269 } 270 return false; 271 } 272 273 /** 274 * Returns the group summary notification. 275 */ 276 @Nullable getGroupSummaryNotification()277 public AlertEntry getGroupSummaryNotification() { 278 return mGroupSummaryNotification; 279 } 280 281 /** 282 * Sets the list of child notification titles. 283 */ setChildTitles(List<String> childTitles)284 public void setChildTitles(List<String> childTitles) { 285 mChildTitles = childTitles; 286 } 287 288 /** 289 * Returns the list of child notification titles. 290 */ 291 @Nullable getChildTitles()292 public List<String> getChildTitles() { 293 return mChildTitles; 294 } 295 296 /** 297 * Generates the list of the child notification titles for a group summary notification. 298 */ generateChildTitles()299 public List<String> generateChildTitles() { 300 List<String> titles = new ArrayList<>(); 301 302 for (AlertEntry notification : mNotifications) { 303 Bundle extras = notification.getNotification().extras; 304 if (extras.containsKey(Notification.EXTRA_TITLE)) { 305 titles.add(extras.getString(Notification.EXTRA_TITLE)); 306 } else if (extras.containsKey(Notification.EXTRA_TITLE_BIG)) { 307 titles.add(extras.getString(Notification.EXTRA_TITLE_BIG)); 308 } else if (extras.containsKey(Notification.EXTRA_MESSAGES)) { 309 List<Notification.MessagingStyle.Message> messages = 310 Notification.MessagingStyle.Message.getMessagesFromBundleArray( 311 extras.getParcelableArray(Notification.EXTRA_MESSAGES)); 312 Notification.MessagingStyle.Message lastMessage = messages.get(messages.size() - 1); 313 titles.add(lastMessage.getSenderPerson().getName().toString()); 314 } else if (extras.containsKey(Notification.EXTRA_SUB_TEXT)) { 315 titles.add(extras.getString(Notification.EXTRA_SUB_TEXT)); 316 } 317 } 318 319 return titles; 320 } 321 322 /** 323 * Returns a single notification that represents this NotificationGroup: 324 * 325 * <p> If the NotificationGroup is a valid grouped notification or has no child notifications, 326 * the group summary notification is returned. 327 * 328 * <p> If the NotificationGroup has only 1 child notification, 329 * or has more than 1 child notifications without a valid group summary, 330 * the first child notification is returned. 331 * 332 * @return the notification that represents this NotificationGroup 333 */ getSingleNotification()334 public AlertEntry getSingleNotification() { 335 if (isGroup() || getChildCount() == 0) { 336 return getGroupSummaryNotification(); 337 } else { 338 return mNotifications.get(0); 339 } 340 } 341 getNotificationForSorting()342 AlertEntry getNotificationForSorting() { 343 if (mGroupSummaryNotification != null) { 344 return getGroupSummaryNotification(); 345 } 346 return getSingleNotification(); 347 } 348 assertSameGroupKey(String groupKey)349 private void assertSameGroupKey(String groupKey) { 350 if (mGroupKey == null) { 351 setGroupKey(groupKey); 352 } else if (!mGroupKey.equals(groupKey)) { 353 updateGroupKeyOrThrowError(groupKey); 354 } 355 } 356 357 /** 358 * If {@link mGroupKey} and the passed groupKey doesn't match, then compare the passed groupKey 359 * with the groupKey of a single notification to decide whether to update or throw an error. 360 */ updateGroupKeyOrThrowError(String groupKey)361 private void updateGroupKeyOrThrowError(String groupKey) { 362 if (getSingleNotification() != null 363 && getSingleNotification().getStatusBarNotification() != null) { 364 String singleGroupKey = 365 getSingleNotification().getStatusBarNotification().getGroupKey(); 366 if (TextUtils.equals(groupKey, singleGroupKey)) { 367 if (DEBUG) { 368 Log.d(TAG, "The current groupKey is: " + mGroupKey + ", and it will be" 369 + "updated to: " + singleGroupKey); 370 } 371 setGroupKey(singleGroupKey); 372 } else { 373 throw new IllegalStateException( 374 "Group key mismatch when adding a notification to a group. " 375 + "mGroupKey: " + mGroupKey + "; groupKey:" + groupKey); 376 } 377 } 378 } 379 380 @Override toString()381 public String toString() { 382 return mGroupKey + ": " + mNotifications; 383 } 384 } 385