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