blob: 91266263f18af4a23638700553ef94933bc0b8f9 [file] [log] [blame]
/*
* Copyright (C) 2017 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.launcher3.notification;
import android.annotation.TargetApi;
import android.app.Notification;
import android.app.NotificationChannel;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.service.notification.NotificationListenerService;
import android.service.notification.StatusBarNotification;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.ArraySet;
import android.util.Log;
import android.util.Pair;
import com.android.launcher3.LauncherModel;
import com.android.launcher3.util.PackageUserKey;
import com.android.launcher3.util.SettingsObserver;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import static com.android.launcher3.SettingsActivity.NOTIFICATION_BADGING;
/**
* A {@link NotificationListenerService} that sends updates to its
* {@link NotificationsChangedListener} when notifications are posted or canceled,
* as well and when this service first connects. An instance of NotificationListener,
* and its methods for getting notifications, can be obtained via {@link #getInstanceIfConnected()}.
*/
@TargetApi(Build.VERSION_CODES.O)
public class NotificationListener extends NotificationListenerService {
public static final String TAG = "NotificationListener";
private static final int MSG_NOTIFICATION_POSTED = 1;
private static final int MSG_NOTIFICATION_REMOVED = 2;
private static final int MSG_NOTIFICATION_FULL_REFRESH = 3;
private static NotificationListener sNotificationListenerInstance = null;
private static NotificationsChangedListener sNotificationsChangedListener;
private static boolean sIsConnected;
private static boolean sIsCreated;
private final Handler mWorkerHandler;
private final Handler mUiHandler;
private final Ranking mTempRanking = new Ranking();
private SettingsObserver mNotificationBadgingObserver;
private final Handler.Callback mWorkerCallback = new Handler.Callback() {
@Override
public boolean handleMessage(Message message) {
switch (message.what) {
case MSG_NOTIFICATION_POSTED:
mUiHandler.obtainMessage(message.what, message.obj).sendToTarget();
break;
case MSG_NOTIFICATION_REMOVED:
mUiHandler.obtainMessage(message.what, message.obj).sendToTarget();
break;
case MSG_NOTIFICATION_FULL_REFRESH:
List<StatusBarNotification> activeNotifications;
if (sIsConnected) {
try {
activeNotifications = filterNotifications(getActiveNotifications());
} catch (SecurityException ex) {
Log.e(TAG, "SecurityException: failed to fetch notifications");
activeNotifications = new ArrayList<StatusBarNotification>();
}
} else {
activeNotifications = new ArrayList<StatusBarNotification>();
}
mUiHandler.obtainMessage(message.what, activeNotifications).sendToTarget();
break;
}
return true;
}
};
private final Handler.Callback mUiCallback = new Handler.Callback() {
@Override
public boolean handleMessage(Message message) {
switch (message.what) {
case MSG_NOTIFICATION_POSTED:
if (sNotificationsChangedListener != null) {
NotificationPostedMsg msg = (NotificationPostedMsg) message.obj;
sNotificationsChangedListener.onNotificationPosted(msg.packageUserKey,
msg.notificationKey, msg.shouldBeFilteredOut);
}
break;
case MSG_NOTIFICATION_REMOVED:
if (sNotificationsChangedListener != null) {
Pair<PackageUserKey, NotificationKeyData> pair
= (Pair<PackageUserKey, NotificationKeyData>) message.obj;
sNotificationsChangedListener.onNotificationRemoved(pair.first, pair.second);
}
break;
case MSG_NOTIFICATION_FULL_REFRESH:
if (sNotificationsChangedListener != null) {
sNotificationsChangedListener.onNotificationFullRefresh(
(List<StatusBarNotification>) message.obj);
}
break;
}
return true;
}
};
public NotificationListener() {
super();
mWorkerHandler = new Handler(LauncherModel.getWorkerLooper(), mWorkerCallback);
mUiHandler = new Handler(Looper.getMainLooper(), mUiCallback);
sNotificationListenerInstance = this;
}
@Override
public void onCreate() {
super.onCreate();
sIsCreated = true;
mNotificationBadgingObserver = new SettingsObserver.Secure(getContentResolver()) {
@Override
public void onSettingChanged(boolean isNotificationBadgingEnabled) {
if (!isNotificationBadgingEnabled) {
requestUnbind();
}
}
};
mNotificationBadgingObserver.register(NOTIFICATION_BADGING);
}
@Override
public void onDestroy() {
super.onDestroy();
sIsCreated = false;
mNotificationBadgingObserver.unregister();
}
public static @Nullable NotificationListener getInstanceIfConnected() {
return sIsConnected ? sNotificationListenerInstance : null;
}
public static void setNotificationsChangedListener(NotificationsChangedListener listener) {
sNotificationsChangedListener = listener;
NotificationListener notificationListener = getInstanceIfConnected();
if (notificationListener != null) {
notificationListener.onNotificationFullRefresh();
} else if (!sIsCreated && sNotificationsChangedListener != null) {
// User turned off badging globally, so we unbound this service;
// tell the listener that there are no notifications to remove dots.
sNotificationsChangedListener.onNotificationFullRefresh(
Collections.<StatusBarNotification>emptyList());
}
}
public static void removeNotificationsChangedListener() {
sNotificationsChangedListener = null;
}
@Override
public void onListenerConnected() {
super.onListenerConnected();
sIsConnected = true;
onNotificationFullRefresh();
}
private void onNotificationFullRefresh() {
mWorkerHandler.obtainMessage(MSG_NOTIFICATION_FULL_REFRESH).sendToTarget();
}
@Override
public void onListenerDisconnected() {
super.onListenerDisconnected();
sIsConnected = false;
}
@Override
public void onNotificationPosted(final StatusBarNotification sbn) {
super.onNotificationPosted(sbn);
mWorkerHandler.obtainMessage(MSG_NOTIFICATION_POSTED, new NotificationPostedMsg(sbn))
.sendToTarget();
}
/**
* An object containing data to send to MSG_NOTIFICATION_POSTED targets.
*/
private class NotificationPostedMsg {
final PackageUserKey packageUserKey;
final NotificationKeyData notificationKey;
final boolean shouldBeFilteredOut;
NotificationPostedMsg(StatusBarNotification sbn) {
packageUserKey = PackageUserKey.fromNotification(sbn);
notificationKey = NotificationKeyData.fromNotification(sbn);
shouldBeFilteredOut = shouldBeFilteredOut(sbn);
}
}
@Override
public void onNotificationRemoved(final StatusBarNotification sbn) {
super.onNotificationRemoved(sbn);
Pair<PackageUserKey, NotificationKeyData> packageUserKeyAndNotificationKey
= new Pair<>(PackageUserKey.fromNotification(sbn),
NotificationKeyData.fromNotification(sbn));
mWorkerHandler.obtainMessage(MSG_NOTIFICATION_REMOVED, packageUserKeyAndNotificationKey)
.sendToTarget();
}
/** This makes a potentially expensive binder call and should be run on a background thread. */
public List<StatusBarNotification> getNotificationsForKeys(List<NotificationKeyData> keys) {
StatusBarNotification[] notifications = NotificationListener.this
.getActiveNotifications(NotificationKeyData.extractKeysOnly(keys)
.toArray(new String[keys.size()]));
return notifications == null
? Collections.<StatusBarNotification>emptyList() : Arrays.asList(notifications);
}
/**
* Filter out notifications that don't have an intent
* or are headers for grouped notifications.
*
* @see #shouldBeFilteredOut(StatusBarNotification)
*/
private List<StatusBarNotification> filterNotifications(
StatusBarNotification[] notifications) {
if (notifications == null) return null;
Set<Integer> removedNotifications = new ArraySet<>();
for (int i = 0; i < notifications.length; i++) {
if (shouldBeFilteredOut(notifications[i])) {
removedNotifications.add(i);
}
}
List<StatusBarNotification> filteredNotifications = new ArrayList<>(
notifications.length - removedNotifications.size());
for (int i = 0; i < notifications.length; i++) {
if (!removedNotifications.contains(i)) {
filteredNotifications.add(notifications[i]);
}
}
return filteredNotifications;
}
private boolean shouldBeFilteredOut(StatusBarNotification sbn) {
getCurrentRanking().getRanking(sbn.getKey(), mTempRanking);
if (!mTempRanking.canShowBadge()) {
return true;
}
Notification notification = sbn.getNotification();
if (mTempRanking.getChannel().getId().equals(NotificationChannel.DEFAULT_CHANNEL_ID)) {
// Special filtering for the default, legacy "Miscellaneous" channel.
if ((notification.flags & Notification.FLAG_ONGOING_EVENT) != 0) {
return true;
}
}
boolean isGroupHeader = (notification.flags & Notification.FLAG_GROUP_SUMMARY) != 0;
CharSequence title = notification.extras.getCharSequence(Notification.EXTRA_TITLE);
CharSequence text = notification.extras.getCharSequence(Notification.EXTRA_TEXT);
boolean missingTitleAndText = TextUtils.isEmpty(title) && TextUtils.isEmpty(text);
return (isGroupHeader || missingTitleAndText);
}
public interface NotificationsChangedListener {
void onNotificationPosted(PackageUserKey postedPackageUserKey,
NotificationKeyData notificationKey, boolean shouldBeFilteredOut);
void onNotificationRemoved(PackageUserKey removedPackageUserKey,
NotificationKeyData notificationKey);
void onNotificationFullRefresh(List<StatusBarNotification> activeNotifications);
}
}