1 /* 2 * Copyright (C) 2021 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.systemui.people; 17 18 import static android.Manifest.permission.READ_CONTACTS; 19 import static android.app.Notification.CATEGORY_MISSED_CALL; 20 import static android.app.Notification.EXTRA_MESSAGES; 21 import static android.app.Notification.EXTRA_PEOPLE_LIST; 22 23 import android.annotation.Nullable; 24 import android.app.Notification; 25 import android.app.Person; 26 import android.content.pm.PackageManager; 27 import android.os.Parcelable; 28 import android.service.notification.StatusBarNotification; 29 import android.util.Log; 30 31 import com.android.internal.annotations.VisibleForTesting; 32 import com.android.internal.util.ArrayUtils; 33 import com.android.systemui.statusbar.notification.collection.NotificationEntry; 34 import com.android.wm.shell.bubbles.Bubbles; 35 36 import java.util.ArrayList; 37 import java.util.Collections; 38 import java.util.Comparator; 39 import java.util.List; 40 import java.util.Objects; 41 import java.util.Optional; 42 import java.util.Set; 43 44 /** Helper functions to handle notifications in People Tiles. */ 45 public class NotificationHelper { 46 private static final boolean DEBUG = PeopleSpaceUtils.DEBUG; 47 private static final String TAG = "PeopleNotifHelper"; 48 49 /** Returns the notification with highest priority to be shown in People Tiles. */ getHighestPriorityNotification( Set<NotificationEntry> notificationEntries)50 public static NotificationEntry getHighestPriorityNotification( 51 Set<NotificationEntry> notificationEntries) { 52 if (notificationEntries == null || notificationEntries.isEmpty()) { 53 return null; 54 } 55 56 return notificationEntries 57 .stream() 58 .filter(NotificationHelper::isMissedCallOrHasContent) 59 .sorted(notificationEntryComparator) 60 .findFirst().orElse(null); 61 } 62 63 64 /** Notification comparator, checking category and timestamps, in reverse order of priority. */ 65 public static Comparator<NotificationEntry> notificationEntryComparator = 66 new Comparator<NotificationEntry>() { 67 @Override 68 public int compare(NotificationEntry e1, NotificationEntry e2) { 69 Notification n1 = e1.getSbn().getNotification(); 70 Notification n2 = e2.getSbn().getNotification(); 71 72 boolean missedCall1 = isMissedCall(n1); 73 boolean missedCall2 = isMissedCall(n2); 74 if (missedCall1 && !missedCall2) { 75 return -1; 76 } 77 if (!missedCall1 && missedCall2) { 78 return 1; 79 } 80 81 // Get messages in reverse chronological order. 82 List<Notification.MessagingStyle.Message> messages1 = 83 getMessagingStyleMessages(n1); 84 List<Notification.MessagingStyle.Message> messages2 = 85 getMessagingStyleMessages(n2); 86 87 if (messages1 != null && messages2 != null) { 88 Notification.MessagingStyle.Message message1 = messages1.get(0); 89 Notification.MessagingStyle.Message message2 = messages2.get(0); 90 return (int) (message2.getTimestamp() - message1.getTimestamp()); 91 } 92 93 if (messages1 == null) { 94 return 1; 95 } 96 if (messages2 == null) { 97 return -1; 98 } 99 return (int) (n2.when - n1.when); 100 } 101 }; 102 103 /** Returns whether {@code e} is a missed call notification. */ isMissedCall(NotificationEntry e)104 public static boolean isMissedCall(NotificationEntry e) { 105 return e != null && e.getSbn().getNotification() != null 106 && isMissedCall(e.getSbn().getNotification()); 107 } 108 109 /** Returns whether {@code notification} is a missed call notification. */ isMissedCall(Notification notification)110 public static boolean isMissedCall(Notification notification) { 111 return notification != null && Objects.equals(notification.category, CATEGORY_MISSED_CALL); 112 } 113 hasContent(NotificationEntry e)114 private static boolean hasContent(NotificationEntry e) { 115 if (e == null) { 116 return false; 117 } 118 List<Notification.MessagingStyle.Message> messages = 119 getMessagingStyleMessages(e.getSbn().getNotification()); 120 return messages != null && !messages.isEmpty(); 121 } 122 123 /** Returns whether {@code e} is a valid conversation notification. */ isValid(NotificationEntry e)124 public static boolean isValid(NotificationEntry e) { 125 return e != null && e.getRanking() != null 126 && e.getRanking().getConversationShortcutInfo() != null 127 && e.getSbn().getNotification() != null; 128 } 129 130 /** Returns whether conversation notification should be shown in People Tile. */ isMissedCallOrHasContent(NotificationEntry e)131 public static boolean isMissedCallOrHasContent(NotificationEntry e) { 132 return isMissedCall(e) || hasContent(e); 133 } 134 135 /** Returns whether {@code sbn}'s package has permission to read contacts. */ hasReadContactsPermission( PackageManager packageManager, StatusBarNotification sbn)136 public static boolean hasReadContactsPermission( 137 PackageManager packageManager, StatusBarNotification sbn) { 138 return packageManager.checkPermission(READ_CONTACTS, 139 sbn.getPackageName()) == PackageManager.PERMISSION_GRANTED; 140 } 141 142 /** 143 * Returns whether a notification should be matched to other Tiles by Uri. 144 * 145 * <p>Currently only matches missed calls. 146 */ shouldMatchNotificationByUri(StatusBarNotification sbn)147 public static boolean shouldMatchNotificationByUri(StatusBarNotification sbn) { 148 Notification notification = sbn.getNotification(); 149 if (notification == null) { 150 if (DEBUG) Log.d(TAG, "Notification is null"); 151 return false; 152 } 153 boolean isMissedCall = isMissedCall(notification); 154 if (!isMissedCall) { 155 if (DEBUG) Log.d(TAG, "Not missed call"); 156 } 157 return isMissedCall; 158 } 159 160 /** 161 * Try to retrieve a valid Uri via {@code sbn}, falling back to the {@code 162 * contactUriFromShortcut} if valid. 163 */ 164 @Nullable getContactUri(StatusBarNotification sbn)165 public static String getContactUri(StatusBarNotification sbn) { 166 // First, try to get a Uri from the Person directly set on the Notification. 167 ArrayList<Person> people = sbn.getNotification().extras.getParcelableArrayList( 168 EXTRA_PEOPLE_LIST); 169 if (people != null && people.get(0) != null) { 170 String contactUri = people.get(0).getUri(); 171 if (contactUri != null && !contactUri.isEmpty()) { 172 return contactUri; 173 } 174 } 175 176 // Then, try to get a Uri from the Person set on the Notification message. 177 List<Notification.MessagingStyle.Message> messages = 178 getMessagingStyleMessages(sbn.getNotification()); 179 if (messages != null && !messages.isEmpty()) { 180 Notification.MessagingStyle.Message message = messages.get(0); 181 Person sender = message.getSenderPerson(); 182 if (sender != null && sender.getUri() != null && !sender.getUri().isEmpty()) { 183 return sender.getUri(); 184 } 185 } 186 187 return null; 188 } 189 190 /** 191 * Returns {@link Notification.MessagingStyle.Message}s from the Notification in chronological 192 * order from most recent to least. 193 */ 194 @VisibleForTesting 195 @Nullable getMessagingStyleMessages( Notification notification)196 public static List<Notification.MessagingStyle.Message> getMessagingStyleMessages( 197 Notification notification) { 198 if (notification == null) { 199 return null; 200 } 201 if (notification.isStyle(Notification.MessagingStyle.class) 202 && notification.extras != null) { 203 final Parcelable[] messages = notification.extras.getParcelableArray(EXTRA_MESSAGES); 204 if (!ArrayUtils.isEmpty(messages)) { 205 List<Notification.MessagingStyle.Message> sortedMessages = 206 Notification.MessagingStyle.Message.getMessagesFromBundleArray(messages); 207 sortedMessages.sort(Collections.reverseOrder( 208 Comparator.comparing(Notification.MessagingStyle.Message::getTimestamp))); 209 return sortedMessages; 210 } 211 } 212 return null; 213 } 214 215 /** Returns whether {@code notification} is a group conversation. */ isGroupConversation(Notification notification)216 private static boolean isGroupConversation(Notification notification) { 217 return notification.extras.getBoolean(Notification.EXTRA_IS_GROUP_CONVERSATION, false); 218 } 219 220 /** 221 * Returns {@code message}'s sender's name if {@code notification} is from a group conversation. 222 */ getSenderIfGroupConversation(Notification notification, Notification.MessagingStyle.Message message)223 public static CharSequence getSenderIfGroupConversation(Notification notification, 224 Notification.MessagingStyle.Message message) { 225 if (!isGroupConversation(notification)) { 226 if (DEBUG) { 227 Log.d(TAG, "Notification is not from a group conversation, not checking sender."); 228 } 229 return null; 230 } 231 Person person = message.getSenderPerson(); 232 if (person == null) { 233 if (DEBUG) Log.d(TAG, "Notification from group conversation doesn't include sender."); 234 return null; 235 } 236 if (DEBUG) Log.d(TAG, "Returning sender from group conversation notification."); 237 return person.getName(); 238 } 239 240 /** Returns whether {@code entry} is suppressed from shade, meaning we should not show it. */ shouldFilterOut( Optional<Bubbles> bubblesOptional, NotificationEntry entry)241 public static boolean shouldFilterOut( 242 Optional<Bubbles> bubblesOptional, NotificationEntry entry) { 243 boolean isSuppressed = false; 244 //TODO(b/190822282): Investigate what is causing the NullPointerException 245 try { 246 isSuppressed = bubblesOptional.isPresent() 247 && bubblesOptional.get().isBubbleNotificationSuppressedFromShade( 248 entry.getKey(), entry.getSbn().getGroupKey()); 249 } catch (Exception e) { 250 Log.e(TAG, "Exception checking if notification is suppressed: " + e); 251 } 252 return isSuppressed; 253 } 254 } 255 256