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