blob: d863dcce4fc765f58f6d26ffb359ac6cdd2de5d1 [file] [log] [blame]
/*
* 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<NotificationEntry> 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<NotificationEntry> notificationEntryComparator =
new Comparator<NotificationEntry>() {
@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<Notification.MessagingStyle.Message> messages1 =
getMessagingStyleMessages(n1);
List<Notification.MessagingStyle.Message> 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<Notification.MessagingStyle.Message> 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.
*
* <p>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<Person> 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<Notification.MessagingStyle.Message> 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<Notification.MessagingStyle.Message> 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<Notification.MessagingStyle.Message> 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<Bubbles> 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;
}
}