/* * 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.config.FeatureFlags; 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 activeNotifications; if (sIsConnected) { try { activeNotifications = filterNotifications(getActiveNotifications()); } catch (SecurityException ex) { Log.e(TAG, "SecurityException: failed to fetch notifications"); activeNotifications = new ArrayList(); } } else { activeNotifications = new ArrayList(); } 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 pair = (Pair) message.obj; sNotificationsChangedListener.onNotificationRemoved(pair.first, pair.second); } break; case MSG_NOTIFICATION_FULL_REFRESH: if (sNotificationsChangedListener != null) { sNotificationsChangedListener.onNotificationFullRefresh( (List) 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) { if (!FeatureFlags.BADGE_ICONS) { return; } 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.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 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 getNotificationsForKeys(List keys) { StatusBarNotification[] notifications = NotificationListener.this .getActiveNotifications(NotificationKeyData.extractKeysOnly(keys) .toArray(new String[keys.size()])); return notifications == null ? Collections.emptyList() : Arrays.asList(notifications); } /** * Filter out notifications that don't have an intent * or are headers for grouped notifications. * * @see #shouldBeFilteredOut(StatusBarNotification) */ private List filterNotifications( StatusBarNotification[] notifications) { if (notifications == null) return null; Set removedNotifications = new ArraySet<>(); for (int i = 0; i < notifications.length; i++) { if (shouldBeFilteredOut(notifications[i])) { removedNotifications.add(i); } } List 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 activeNotifications); } }