1 /* 2 * Copyright (C) 2017 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 17 package com.android.launcher3.notification; 18 19 import android.app.Notification; 20 import android.app.NotificationChannel; 21 import android.os.Handler; 22 import android.os.Looper; 23 import android.os.Message; 24 import android.service.notification.NotificationListenerService; 25 import android.service.notification.StatusBarNotification; 26 import android.support.annotation.Nullable; 27 import android.support.v4.util.Pair; 28 import android.text.TextUtils; 29 30 import com.android.launcher3.LauncherModel; 31 import com.android.launcher3.config.FeatureFlags; 32 import com.android.launcher3.util.PackageUserKey; 33 34 import java.util.ArrayList; 35 import java.util.Arrays; 36 import java.util.Collections; 37 import java.util.HashSet; 38 import java.util.List; 39 import java.util.Set; 40 41 /** 42 * A {@link NotificationListenerService} that sends updates to its 43 * {@link NotificationsChangedListener} when notifications are posted or canceled, 44 * as well and when this service first connects. An instance of NotificationListener, 45 * and its methods for getting notifications, can be obtained via {@link #getInstanceIfConnected()}. 46 */ 47 public class NotificationListener extends NotificationListenerService { 48 49 private static final int MSG_NOTIFICATION_POSTED = 1; 50 private static final int MSG_NOTIFICATION_REMOVED = 2; 51 private static final int MSG_NOTIFICATION_FULL_REFRESH = 3; 52 53 private static NotificationListener sNotificationListenerInstance = null; 54 private static NotificationsChangedListener sNotificationsChangedListener; 55 private static boolean sIsConnected; 56 57 private final Handler mWorkerHandler; 58 private final Handler mUiHandler; 59 60 private Ranking mTempRanking = new Ranking(); 61 62 private Handler.Callback mWorkerCallback = new Handler.Callback() { 63 @Override 64 public boolean handleMessage(Message message) { 65 switch (message.what) { 66 case MSG_NOTIFICATION_POSTED: 67 mUiHandler.obtainMessage(message.what, message.obj).sendToTarget(); 68 break; 69 case MSG_NOTIFICATION_REMOVED: 70 mUiHandler.obtainMessage(message.what, message.obj).sendToTarget(); 71 break; 72 case MSG_NOTIFICATION_FULL_REFRESH: 73 final List<StatusBarNotification> activeNotifications = sIsConnected 74 ? filterNotifications(getActiveNotifications()) 75 : new ArrayList<StatusBarNotification>(); 76 mUiHandler.obtainMessage(message.what, activeNotifications).sendToTarget(); 77 break; 78 } 79 return true; 80 } 81 }; 82 83 private Handler.Callback mUiCallback = new Handler.Callback() { 84 @Override 85 public boolean handleMessage(Message message) { 86 switch (message.what) { 87 case MSG_NOTIFICATION_POSTED: 88 if (sNotificationsChangedListener != null) { 89 NotificationPostedMsg msg = (NotificationPostedMsg) message.obj; 90 sNotificationsChangedListener.onNotificationPosted(msg.packageUserKey, 91 msg.notificationKey, msg.shouldBeFilteredOut); 92 } 93 break; 94 case MSG_NOTIFICATION_REMOVED: 95 if (sNotificationsChangedListener != null) { 96 Pair<PackageUserKey, NotificationKeyData> pair 97 = (Pair<PackageUserKey, NotificationKeyData>) message.obj; 98 sNotificationsChangedListener.onNotificationRemoved(pair.first, pair.second); 99 } 100 break; 101 case MSG_NOTIFICATION_FULL_REFRESH: 102 if (sNotificationsChangedListener != null) { 103 sNotificationsChangedListener.onNotificationFullRefresh( 104 (List<StatusBarNotification>) message.obj); 105 } 106 break; 107 } 108 return true; 109 } 110 }; 111 NotificationListener()112 public NotificationListener() { 113 super(); 114 mWorkerHandler = new Handler(LauncherModel.getWorkerLooper(), mWorkerCallback); 115 mUiHandler = new Handler(Looper.getMainLooper(), mUiCallback); 116 sNotificationListenerInstance = this; 117 } 118 getInstanceIfConnected()119 public static @Nullable NotificationListener getInstanceIfConnected() { 120 return sIsConnected ? sNotificationListenerInstance : null; 121 } 122 setNotificationsChangedListener(NotificationsChangedListener listener)123 public static void setNotificationsChangedListener(NotificationsChangedListener listener) { 124 if (!FeatureFlags.BADGE_ICONS) { 125 return; 126 } 127 sNotificationsChangedListener = listener; 128 129 if (sNotificationListenerInstance != null) { 130 sNotificationListenerInstance.onNotificationFullRefresh(); 131 } 132 } 133 removeNotificationsChangedListener()134 public static void removeNotificationsChangedListener() { 135 sNotificationsChangedListener = null; 136 } 137 138 @Override onListenerConnected()139 public void onListenerConnected() { 140 super.onListenerConnected(); 141 sIsConnected = true; 142 onNotificationFullRefresh(); 143 } 144 onNotificationFullRefresh()145 private void onNotificationFullRefresh() { 146 mWorkerHandler.obtainMessage(MSG_NOTIFICATION_FULL_REFRESH).sendToTarget(); 147 } 148 149 @Override onListenerDisconnected()150 public void onListenerDisconnected() { 151 super.onListenerDisconnected(); 152 sIsConnected = false; 153 } 154 155 @Override onNotificationPosted(final StatusBarNotification sbn)156 public void onNotificationPosted(final StatusBarNotification sbn) { 157 super.onNotificationPosted(sbn); 158 mWorkerHandler.obtainMessage(MSG_NOTIFICATION_POSTED, new NotificationPostedMsg(sbn)) 159 .sendToTarget(); 160 } 161 162 /** 163 * An object containing data to send to MSG_NOTIFICATION_POSTED targets. 164 */ 165 private class NotificationPostedMsg { 166 PackageUserKey packageUserKey; 167 NotificationKeyData notificationKey; 168 boolean shouldBeFilteredOut; 169 NotificationPostedMsg(StatusBarNotification sbn)170 NotificationPostedMsg(StatusBarNotification sbn) { 171 packageUserKey = PackageUserKey.fromNotification(sbn); 172 notificationKey = NotificationKeyData.fromNotification(sbn); 173 shouldBeFilteredOut = shouldBeFilteredOut(sbn); 174 } 175 } 176 177 @Override onNotificationRemoved(final StatusBarNotification sbn)178 public void onNotificationRemoved(final StatusBarNotification sbn) { 179 super.onNotificationRemoved(sbn); 180 Pair<PackageUserKey, NotificationKeyData> packageUserKeyAndNotificationKey 181 = new Pair<>(PackageUserKey.fromNotification(sbn), 182 NotificationKeyData.fromNotification(sbn)); 183 mWorkerHandler.obtainMessage(MSG_NOTIFICATION_REMOVED, packageUserKeyAndNotificationKey) 184 .sendToTarget(); 185 } 186 187 /** This makes a potentially expensive binder call and should be run on a background thread. */ getNotificationsForKeys(List<NotificationKeyData> keys)188 public List<StatusBarNotification> getNotificationsForKeys(List<NotificationKeyData> keys) { 189 StatusBarNotification[] notifications = NotificationListener.this 190 .getActiveNotifications(NotificationKeyData.extractKeysOnly(keys) 191 .toArray(new String[keys.size()])); 192 return notifications == null ? Collections.EMPTY_LIST : Arrays.asList(notifications); 193 } 194 195 /** 196 * Filter out notifications that don't have an intent 197 * or are headers for grouped notifications. 198 * 199 * @see #shouldBeFilteredOut(StatusBarNotification) 200 */ filterNotifications( StatusBarNotification[] notifications)201 private List<StatusBarNotification> filterNotifications( 202 StatusBarNotification[] notifications) { 203 if (notifications == null) return null; 204 Set<Integer> removedNotifications = new HashSet<>(); 205 for (int i = 0; i < notifications.length; i++) { 206 if (shouldBeFilteredOut(notifications[i])) { 207 removedNotifications.add(i); 208 } 209 } 210 List<StatusBarNotification> filteredNotifications = new ArrayList<>( 211 notifications.length - removedNotifications.size()); 212 for (int i = 0; i < notifications.length; i++) { 213 if (!removedNotifications.contains(i)) { 214 filteredNotifications.add(notifications[i]); 215 } 216 } 217 return filteredNotifications; 218 } 219 shouldBeFilteredOut(StatusBarNotification sbn)220 private boolean shouldBeFilteredOut(StatusBarNotification sbn) { 221 getCurrentRanking().getRanking(sbn.getKey(), mTempRanking); 222 if (!mTempRanking.canShowBadge()) { 223 return true; 224 } 225 Notification notification = sbn.getNotification(); 226 if (mTempRanking.getChannel().getId().equals(NotificationChannel.DEFAULT_CHANNEL_ID)) { 227 // Special filtering for the default, legacy "Miscellaneous" channel. 228 if ((notification.flags & Notification.FLAG_ONGOING_EVENT) != 0) { 229 return true; 230 } 231 } 232 boolean isGroupHeader = (notification.flags & Notification.FLAG_GROUP_SUMMARY) != 0; 233 CharSequence title = notification.extras.getCharSequence(Notification.EXTRA_TITLE); 234 CharSequence text = notification.extras.getCharSequence(Notification.EXTRA_TEXT); 235 boolean missingTitleAndText = TextUtils.isEmpty(title) && TextUtils.isEmpty(text); 236 return (isGroupHeader || missingTitleAndText); 237 } 238 239 public interface NotificationsChangedListener { onNotificationPosted(PackageUserKey postedPackageUserKey, NotificationKeyData notificationKey, boolean shouldBeFilteredOut)240 void onNotificationPosted(PackageUserKey postedPackageUserKey, 241 NotificationKeyData notificationKey, boolean shouldBeFilteredOut); onNotificationRemoved(PackageUserKey removedPackageUserKey, NotificationKeyData notificationKey)242 void onNotificationRemoved(PackageUserKey removedPackageUserKey, 243 NotificationKeyData notificationKey); onNotificationFullRefresh(List<StatusBarNotification> activeNotifications)244 void onNotificationFullRefresh(List<StatusBarNotification> activeNotifications); 245 } 246 } 247