/* * Copyright (C) 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.car.notification; import android.app.Notification; import android.content.Context; import android.os.Bundle; import androidx.annotation.NonNull; import androidx.recyclerview.widget.DiffUtil; import com.android.car.ui.recyclerview.ContentLimitingAdapter; import java.util.List; import java.util.Objects; /** * {@link DiffUtil} for car notifications. * This class is not intended for general usage except for the static methods. * *

Two notifications are considered the same if they have the same: *

    *
  1. GroupKey *
  2. Number of AlertEntry contained *
  3. The order of each AlertEntry *
  4. The identifier of each individual AlertEntry contained *
  5. The content of each individual AlertEntry contained *
*/ class CarNotificationDiff extends DiffUtil.Callback { private final Context mContext; private final List mOldList; private final List mNewList; private final int mMaxItems; private boolean mShowRecentsAndOlderHeaders; CarNotificationDiff(Context context, @NonNull List oldList, @NonNull List newList) { this(context, oldList, newList, ContentLimitingAdapter.UNLIMITED); } CarNotificationDiff(Context context, @NonNull List oldList, @NonNull List newList, int maxItems) { mContext = context; mOldList = oldList; mNewList = newList; mMaxItems = maxItems; } void setShowRecentsAndOlderHeaders(boolean showRecentsAndOlderHeaders) { mShowRecentsAndOlderHeaders = showRecentsAndOlderHeaders; } @Override public int getOldListSize() { return getContentLimitedListSize(mOldList.size()); } @Override public int getNewListSize() { return getContentLimitedListSize(mNewList.size()); } @Override public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { NotificationGroup oldItem = mOldList.get(oldItemPosition); NotificationGroup newItem = mNewList.get(newItemPosition); return sameGroupUniqueIdentifiers(oldItem, newItem, mShowRecentsAndOlderHeaders); } /** * Shallow comparison for {@link NotificationGroup}. *

* Checks if two grouped notifications have the same: *

    *
  1. GroupKey *
  2. GroupSummaryKey *
*

* This method does not check for child AlertEntries because child itself will take care of it. * * @param showRecentsAndOlderHeaders if {@code true} then isSeen values of the two notification * groups are also compared. */ static boolean sameGroupUniqueIdentifiers(NotificationGroup oldItem, NotificationGroup newItem, boolean showRecentsAndOlderHeaders) { if (oldItem == newItem) { return true; } if (!oldItem.getGroupKey().equals(newItem.getGroupKey())) { return false; } if (showRecentsAndOlderHeaders) { if (oldItem.isSeen() != newItem.isSeen()) { return false; } } return sameNotificationKey( oldItem.getGroupSummaryNotification(), newItem.getGroupSummaryNotification()); } /** * Shallow comparison for {@link AlertEntry}: only comparing the unique IDs. * *

Returns true if two notifications have the same key. */ static boolean sameNotificationKey(AlertEntry oldItem, AlertEntry newItem) { if (oldItem == newItem) { return true; } return oldItem != null && newItem != null && Objects.equals(oldItem.getKey(), newItem.getKey()); } /** * Shallow comparison for {@link AlertEntry}: comparing the unique IDs and the * notification Flags. * *

Returns true if two notifications have the same key and notification flags. */ static boolean sameNotificationKeyAndFlags(AlertEntry oldItem, AlertEntry newItem) { return sameNotificationKey(oldItem, newItem) && oldItem.getNotification().flags == newItem.getNotification().flags; } /** * Deep comparison for {@link NotificationGroup}. * *

Compare the size and contents of each AlertEntry inside the NotificationGroup. * *

This method will only be called if {@link #areItemsTheSame} returns true. */ @Override public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { NotificationGroup oldItem = mOldList.get(oldItemPosition); NotificationGroup newItem = mNewList.get(newItemPosition); // Header and Footer should always refresh if some notification items have changed. if (newItem.isHeaderOrFooter()) { return false; } if (!sameNotificationContent( oldItem.getGroupSummaryNotification(), newItem.getGroupSummaryNotification())) { return false; } if (oldItem.getChildCount() != newItem.getChildCount()) { return false; } List oldChildNotifications = oldItem.getChildNotifications(); List newChildNotifications = newItem.getChildNotifications(); for (int i = 0; i < oldItem.getChildCount(); i++) { AlertEntry oldNotification = oldChildNotifications.get(i); AlertEntry newNotification = newChildNotifications.get(i); if (!sameNotificationContent(oldNotification, newNotification)) { return false; } } return true; } /** * Deep comparison for {@link AlertEntry}. * *

We are only comparing a subset of the fields that have visible effects on our product. * Most of the deprecated fields are not compared. * Fields that do not have visible effects (e.g. privacy-related) are ignored for now. */ private boolean sameNotificationContent(AlertEntry oldItem, AlertEntry newItem) { if (oldItem == newItem) { return true; } if (oldItem == null || newItem == null) { return false; } if (oldItem.getStatusBarNotification().isGroup() != newItem.getStatusBarNotification().isGroup() || oldItem.getStatusBarNotification().isClearable() != newItem.getStatusBarNotification().isClearable() || oldItem.getStatusBarNotification().isOngoing() != newItem.getStatusBarNotification().isOngoing()) { return false; } Notification oldNotification = oldItem.getNotification(); Notification newNotification = newItem.getNotification(); if (oldNotification.flags != newNotification.flags || oldNotification.category != newNotification.category || oldNotification.color != newNotification.color || !areBundlesEqual(oldNotification.extras, newNotification.extras) || !Objects.equals(oldNotification.contentIntent, newNotification.contentIntent) || !Objects.equals(oldNotification.deleteIntent, newNotification.deleteIntent) || !Objects.equals( oldNotification.fullScreenIntent, newNotification.fullScreenIntent) || !Objects.deepEquals(oldNotification.actions, newNotification.actions)) { return false; } // Recover builders only until the above if-statements fail Notification.Builder oldBuilder = Notification.Builder.recoverBuilder(mContext, oldNotification); Notification.Builder newBuilder = Notification.Builder.recoverBuilder(mContext, newNotification); return !Notification.areStyledNotificationsVisiblyDifferent(oldBuilder, newBuilder); } private boolean areBundlesEqual(Bundle oldBundle, Bundle newBundle) { if (oldBundle.size() != newBundle.size()) { return false; } for (String key : oldBundle.keySet()) { if (!newBundle.containsKey(key)) { return false; } Object oldValue = oldBundle.get(key); Object newValue = newBundle.get(key); if (!Objects.equals(oldValue, newValue)) { return false; } } return true; } private int getContentLimitedListSize(int listSize) { if (mMaxItems != ContentLimitingAdapter.UNLIMITED) { // Add one to mMaxItems to account for the scrolling limited message that is added by // the ContentLimitingAdapter. return Math.min(listSize, mMaxItems + 1); } return listSize; } }