• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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