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