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.app.Notification; 19 import android.content.Context; 20 import android.os.Bundle; 21 22 import androidx.annotation.NonNull; 23 import androidx.recyclerview.widget.DiffUtil; 24 25 import com.android.car.ui.recyclerview.ContentLimitingAdapter; 26 27 import java.util.List; 28 import java.util.Objects; 29 30 /** 31 * {@link DiffUtil} for car notifications. 32 * This class is not intended for general usage except for the static methods. 33 * 34 * <p> Two notifications are considered the same if they have the same: 35 * <ol> 36 * <li> GroupKey 37 * <li> Number of AlertEntry contained 38 * <li> The order of each AlertEntry 39 * <li> The identifier of each individual AlertEntry contained 40 * <li> The content of each individual AlertEntry contained 41 * </ol> 42 */ 43 class CarNotificationDiff extends DiffUtil.Callback { 44 private final Context mContext; 45 private final List<NotificationGroup> mOldList; 46 private final List<NotificationGroup> mNewList; 47 private final int mMaxItems; 48 private boolean mShowRecentsAndOlderHeaders; 49 CarNotificationDiff(Context context, @NonNull List<NotificationGroup> oldList, @NonNull List<NotificationGroup> newList)50 CarNotificationDiff(Context context, @NonNull List<NotificationGroup> oldList, 51 @NonNull List<NotificationGroup> newList) { 52 this(context, oldList, newList, ContentLimitingAdapter.UNLIMITED); 53 } 54 CarNotificationDiff(Context context, @NonNull List<NotificationGroup> oldList, @NonNull List<NotificationGroup> newList, int maxItems)55 CarNotificationDiff(Context context, @NonNull List<NotificationGroup> oldList, 56 @NonNull List<NotificationGroup> newList, int maxItems) { 57 mContext = context; 58 mOldList = oldList; 59 mNewList = newList; 60 mMaxItems = maxItems; 61 } 62 setShowRecentsAndOlderHeaders(boolean showRecentsAndOlderHeaders)63 void setShowRecentsAndOlderHeaders(boolean showRecentsAndOlderHeaders) { 64 mShowRecentsAndOlderHeaders = showRecentsAndOlderHeaders; 65 } 66 67 @Override getOldListSize()68 public int getOldListSize() { 69 return getContentLimitedListSize(mOldList.size()); 70 } 71 72 @Override getNewListSize()73 public int getNewListSize() { 74 return getContentLimitedListSize(mNewList.size()); 75 } 76 77 @Override areItemsTheSame(int oldItemPosition, int newItemPosition)78 public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { 79 NotificationGroup oldItem = mOldList.get(oldItemPosition); 80 NotificationGroup newItem = mNewList.get(newItemPosition); 81 82 return sameGroupUniqueIdentifiers(oldItem, newItem, mShowRecentsAndOlderHeaders); 83 } 84 85 /** 86 * Shallow comparison for {@link NotificationGroup}. 87 * <p> 88 * Checks if two grouped notifications have the same: 89 * <ol> 90 * <li> GroupKey 91 * <li> GroupSummaryKey 92 * </ol> 93 * <p> 94 * This method does not check for child AlertEntries because child itself will take care of it. 95 * 96 * @param showRecentsAndOlderHeaders if {@code true} then isSeen values of the two notification 97 * groups are also compared. 98 */ sameGroupUniqueIdentifiers(NotificationGroup oldItem, NotificationGroup newItem, boolean showRecentsAndOlderHeaders)99 static boolean sameGroupUniqueIdentifiers(NotificationGroup oldItem, 100 NotificationGroup newItem, boolean showRecentsAndOlderHeaders) { 101 102 if (oldItem == newItem) { 103 return true; 104 } 105 106 if (!oldItem.getGroupKey().equals(newItem.getGroupKey())) { 107 return false; 108 } 109 110 if (showRecentsAndOlderHeaders) { 111 if (oldItem.isSeen() != newItem.isSeen()) { 112 return false; 113 } 114 } 115 116 return sameNotificationKey( 117 oldItem.getGroupSummaryNotification(), newItem.getGroupSummaryNotification()); 118 } 119 120 /** 121 * Shallow comparison for {@link AlertEntry}: only comparing the unique IDs. 122 * 123 * <p> Returns true if two notifications have the same key. 124 */ sameNotificationKey(AlertEntry oldItem, AlertEntry newItem)125 static boolean sameNotificationKey(AlertEntry oldItem, AlertEntry newItem) { 126 if (oldItem == newItem) { 127 return true; 128 } 129 130 return oldItem != null 131 && newItem != null 132 && Objects.equals(oldItem.getKey(), newItem.getKey()); 133 } 134 135 /** 136 * Shallow comparison for {@link AlertEntry}: comparing the unique IDs and the 137 * notification Flags. 138 * 139 * <p> Returns true if two notifications have the same key and notification flags. 140 */ sameNotificationKeyAndFlags(AlertEntry oldItem, AlertEntry newItem)141 static boolean sameNotificationKeyAndFlags(AlertEntry oldItem, AlertEntry newItem) { 142 return sameNotificationKey(oldItem, newItem) 143 && oldItem.getNotification().flags == newItem.getNotification().flags; 144 } 145 146 /** 147 * Deep comparison for {@link NotificationGroup}. 148 * 149 * <p> Compare the size and contents of each AlertEntry inside the NotificationGroup. 150 * 151 * <p> This method will only be called if {@link #areItemsTheSame} returns true. 152 */ 153 @Override areContentsTheSame(int oldItemPosition, int newItemPosition)154 public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { 155 NotificationGroup oldItem = mOldList.get(oldItemPosition); 156 NotificationGroup newItem = mNewList.get(newItemPosition); 157 158 // Header and Footer should always refresh if some notification items have changed. 159 if (newItem.isHeaderOrFooter()) { 160 return false; 161 } 162 163 if (!sameNotificationContent( 164 oldItem.getGroupSummaryNotification(), newItem.getGroupSummaryNotification())) { 165 return false; 166 } 167 168 if (oldItem.getChildCount() != newItem.getChildCount()) { 169 return false; 170 } 171 172 List<AlertEntry> oldChildNotifications = oldItem.getChildNotifications(); 173 List<AlertEntry> newChildNotifications = newItem.getChildNotifications(); 174 175 for (int i = 0; i < oldItem.getChildCount(); i++) { 176 AlertEntry oldNotification = oldChildNotifications.get(i); 177 AlertEntry newNotification = newChildNotifications.get(i); 178 if (!sameNotificationContent(oldNotification, newNotification)) { 179 return false; 180 } 181 } 182 183 return true; 184 } 185 186 /** 187 * Deep comparison for {@link AlertEntry}. 188 * 189 * <p> We are only comparing a subset of the fields that have visible effects on our product. 190 * Most of the deprecated fields are not compared. 191 * Fields that do not have visible effects (e.g. privacy-related) are ignored for now. 192 */ sameNotificationContent(AlertEntry oldItem, AlertEntry newItem)193 private boolean sameNotificationContent(AlertEntry oldItem, AlertEntry newItem) { 194 195 if (oldItem == newItem) { 196 return true; 197 } 198 199 if (oldItem == null || newItem == null) { 200 return false; 201 } 202 203 if (oldItem.getStatusBarNotification().isGroup() 204 != newItem.getStatusBarNotification().isGroup() 205 || oldItem.getStatusBarNotification().isClearable() 206 != newItem.getStatusBarNotification().isClearable() 207 || oldItem.getStatusBarNotification().isOngoing() 208 != newItem.getStatusBarNotification().isOngoing()) { 209 return false; 210 } 211 212 Notification oldNotification = oldItem.getNotification(); 213 Notification newNotification = newItem.getNotification(); 214 215 if (oldNotification.flags != newNotification.flags 216 || oldNotification.category != newNotification.category 217 || oldNotification.color != newNotification.color 218 || !areBundlesEqual(oldNotification.extras, newNotification.extras) 219 || !Objects.equals(oldNotification.contentIntent, newNotification.contentIntent) 220 || !Objects.equals(oldNotification.deleteIntent, newNotification.deleteIntent) 221 || !Objects.equals( 222 oldNotification.fullScreenIntent, newNotification.fullScreenIntent) 223 || !Objects.deepEquals(oldNotification.actions, newNotification.actions)) { 224 return false; 225 } 226 227 // Recover builders only until the above if-statements fail 228 Notification.Builder oldBuilder = 229 Notification.Builder.recoverBuilder(mContext, oldNotification); 230 Notification.Builder newBuilder = 231 Notification.Builder.recoverBuilder(mContext, newNotification); 232 233 return !Notification.areStyledNotificationsVisiblyDifferent(oldBuilder, newBuilder); 234 } 235 areBundlesEqual(Bundle oldBundle, Bundle newBundle)236 private boolean areBundlesEqual(Bundle oldBundle, Bundle newBundle) { 237 if (oldBundle.size() != newBundle.size()) { 238 return false; 239 } 240 241 for (String key : oldBundle.keySet()) { 242 if (!newBundle.containsKey(key)) { 243 return false; 244 } 245 246 Object oldValue = oldBundle.get(key); 247 Object newValue = newBundle.get(key); 248 if (!Objects.equals(oldValue, newValue)) { 249 return false; 250 } 251 } 252 253 return true; 254 } 255 getContentLimitedListSize(int listSize)256 private int getContentLimitedListSize(int listSize) { 257 if (mMaxItems != ContentLimitingAdapter.UNLIMITED) { 258 // Add one to mMaxItems to account for the scrolling limited message that is added by 259 // the ContentLimitingAdapter. 260 return Math.min(listSize, mMaxItems + 1); 261 } 262 return listSize; 263 } 264 } 265