/* * Copyright (C) 2021 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.systemui.people; import static android.Manifest.permission.READ_CONTACTS; import static android.app.Notification.CATEGORY_MISSED_CALL; import static android.app.Notification.EXTRA_MESSAGES; import static android.app.Notification.EXTRA_PEOPLE_LIST; import android.annotation.Nullable; import android.app.Notification; import android.app.Person; import android.content.pm.PackageManager; import android.os.Parcelable; import android.service.notification.StatusBarNotification; import android.util.Log; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ArrayUtils; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.wm.shell.bubbles.Bubbles; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.Set; /** Helper functions to handle notifications in People Tiles. */ public class NotificationHelper { private static final boolean DEBUG = PeopleSpaceUtils.DEBUG; private static final String TAG = "PeopleNotifHelper"; /** Returns the notification with highest priority to be shown in People Tiles. */ public static NotificationEntry getHighestPriorityNotification( Set notificationEntries) { if (notificationEntries == null || notificationEntries.isEmpty()) { return null; } return notificationEntries .stream() .filter(NotificationHelper::isMissedCallOrHasContent) .sorted(notificationEntryComparator) .findFirst().orElse(null); } /** Notification comparator, checking category and timestamps, in reverse order of priority. */ public static Comparator notificationEntryComparator = new Comparator() { @Override public int compare(NotificationEntry e1, NotificationEntry e2) { Notification n1 = e1.getSbn().getNotification(); Notification n2 = e2.getSbn().getNotification(); boolean missedCall1 = isMissedCall(n1); boolean missedCall2 = isMissedCall(n2); if (missedCall1 && !missedCall2) { return -1; } if (!missedCall1 && missedCall2) { return 1; } // Get messages in reverse chronological order. List messages1 = getMessagingStyleMessages(n1); List messages2 = getMessagingStyleMessages(n2); if (messages1 != null && messages2 != null) { Notification.MessagingStyle.Message message1 = messages1.get(0); Notification.MessagingStyle.Message message2 = messages2.get(0); return (int) (message2.getTimestamp() - message1.getTimestamp()); } if (messages1 == null) { return 1; } if (messages2 == null) { return -1; } return (int) (n2.when - n1.when); } }; /** Returns whether {@code e} is a missed call notification. */ public static boolean isMissedCall(NotificationEntry e) { return e != null && e.getSbn().getNotification() != null && isMissedCall(e.getSbn().getNotification()); } /** Returns whether {@code notification} is a missed call notification. */ public static boolean isMissedCall(Notification notification) { return notification != null && Objects.equals(notification.category, CATEGORY_MISSED_CALL); } private static boolean hasContent(NotificationEntry e) { if (e == null) { return false; } List messages = getMessagingStyleMessages(e.getSbn().getNotification()); return messages != null && !messages.isEmpty(); } /** Returns whether {@code e} is a valid conversation notification. */ public static boolean isValid(NotificationEntry e) { return e != null && e.getRanking() != null && e.getRanking().getConversationShortcutInfo() != null && e.getSbn().getNotification() != null; } /** Returns whether conversation notification should be shown in People Tile. */ public static boolean isMissedCallOrHasContent(NotificationEntry e) { return isMissedCall(e) || hasContent(e); } /** Returns whether {@code sbn}'s package has permission to read contacts. */ public static boolean hasReadContactsPermission( PackageManager packageManager, StatusBarNotification sbn) { return packageManager.checkPermission(READ_CONTACTS, sbn.getPackageName()) == PackageManager.PERMISSION_GRANTED; } /** * Returns whether a notification should be matched to other Tiles by Uri. * *

Currently only matches missed calls. */ public static boolean shouldMatchNotificationByUri(StatusBarNotification sbn) { Notification notification = sbn.getNotification(); if (notification == null) { if (DEBUG) Log.d(TAG, "Notification is null"); return false; } boolean isMissedCall = isMissedCall(notification); if (!isMissedCall) { if (DEBUG) Log.d(TAG, "Not missed call"); } return isMissedCall; } /** * Try to retrieve a valid Uri via {@code sbn}, falling back to the {@code * contactUriFromShortcut} if valid. */ @Nullable public static String getContactUri(StatusBarNotification sbn) { // First, try to get a Uri from the Person directly set on the Notification. ArrayList people = sbn.getNotification().extras.getParcelableArrayList( EXTRA_PEOPLE_LIST); if (people != null && people.get(0) != null) { String contactUri = people.get(0).getUri(); if (contactUri != null && !contactUri.isEmpty()) { return contactUri; } } // Then, try to get a Uri from the Person set on the Notification message. List messages = getMessagingStyleMessages(sbn.getNotification()); if (messages != null && !messages.isEmpty()) { Notification.MessagingStyle.Message message = messages.get(0); Person sender = message.getSenderPerson(); if (sender != null && sender.getUri() != null && !sender.getUri().isEmpty()) { return sender.getUri(); } } return null; } /** * Returns {@link Notification.MessagingStyle.Message}s from the Notification in chronological * order from most recent to least. */ @VisibleForTesting @Nullable public static List getMessagingStyleMessages( Notification notification) { if (notification == null) { return null; } if (notification.isStyle(Notification.MessagingStyle.class) && notification.extras != null) { final Parcelable[] messages = notification.extras.getParcelableArray(EXTRA_MESSAGES); if (!ArrayUtils.isEmpty(messages)) { List sortedMessages = Notification.MessagingStyle.Message.getMessagesFromBundleArray(messages); sortedMessages.sort(Collections.reverseOrder( Comparator.comparing(Notification.MessagingStyle.Message::getTimestamp))); return sortedMessages; } } return null; } /** Returns whether {@code notification} is a group conversation. */ private static boolean isGroupConversation(Notification notification) { return notification.extras.getBoolean(Notification.EXTRA_IS_GROUP_CONVERSATION, false); } /** * Returns {@code message}'s sender's name if {@code notification} is from a group conversation. */ public static CharSequence getSenderIfGroupConversation(Notification notification, Notification.MessagingStyle.Message message) { if (!isGroupConversation(notification)) { if (DEBUG) { Log.d(TAG, "Notification is not from a group conversation, not checking sender."); } return null; } Person person = message.getSenderPerson(); if (person == null) { if (DEBUG) Log.d(TAG, "Notification from group conversation doesn't include sender."); return null; } if (DEBUG) Log.d(TAG, "Returning sender from group conversation notification."); return person.getName(); } /** Returns whether {@code entry} is suppressed from shade, meaning we should not show it. */ public static boolean shouldFilterOut( Optional bubblesOptional, NotificationEntry entry) { boolean isSuppressed = false; //TODO(b/190822282): Investigate what is causing the NullPointerException try { isSuppressed = bubblesOptional.isPresent() && bubblesOptional.get().isBubbleNotificationSuppressedFromShade( entry.getKey(), entry.getSbn().getGroupKey()); } catch (Exception e) { Log.e(TAG, "Exception checking if notification is suppressed: " + e); } return isSuppressed; } }