/* * 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.systemui.statusbar; import static com.android.systemui.statusbar.NotificationRemoteInputManager.ENABLE_REMOTE_INPUT; import static com.android.systemui.statusbar.NotificationRemoteInputManager .FORCE_REMOTE_INPUT_HISTORY; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.database.ContentObserver; import android.os.Build; import android.os.PowerManager; import android.os.RemoteException; import android.os.ServiceManager; import android.os.SystemClock; import android.os.UserHandle; import android.provider.Settings; import android.service.notification.NotificationListenerService; import android.service.notification.NotificationStats; import android.service.notification.StatusBarNotification; import android.text.TextUtils; import android.util.ArraySet; import android.util.ArrayMap; import android.util.EventLog; import android.util.Log; import android.view.View; import android.view.ViewGroup; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.MetricsLogger; import com.android.internal.statusbar.IStatusBarService; import com.android.internal.statusbar.NotificationVisibility; import com.android.internal.util.NotificationMessagingUtil; import com.android.systemui.DejankUtils; import com.android.systemui.Dependency; import com.android.systemui.Dumpable; import com.android.systemui.EventLogTags; import com.android.systemui.ForegroundServiceController; import com.android.systemui.R; import com.android.systemui.UiOffloadThread; import com.android.systemui.recents.misc.SystemServicesProxy; import com.android.systemui.statusbar.notification.InflationException; import com.android.systemui.statusbar.notification.NotificationInflater; import com.android.systemui.statusbar.notification.RowInflaterTask; import com.android.systemui.statusbar.notification.VisualStabilityManager; import com.android.systemui.statusbar.phone.NotificationGroupManager; import com.android.systemui.statusbar.phone.StatusBar; import com.android.systemui.statusbar.policy.DeviceProvisionedController; import com.android.systemui.statusbar.policy.HeadsUpManager; import com.android.systemui.util.leak.LeakDetector; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; /** * NotificationEntryManager is responsible for the adding, removing, and updating of notifications. * It also handles tasks such as their inflation and their interaction with other * Notification.*Manager objects. */ public class NotificationEntryManager implements Dumpable, NotificationInflater.InflationCallback, ExpandableNotificationRow.ExpansionLogger, NotificationUpdateHandler, VisualStabilityManager.Callback { private static final String TAG = "NotificationEntryMgr"; protected static final boolean DEBUG = false; protected static final boolean ENABLE_HEADS_UP = true; protected static final String SETTING_HEADS_UP_TICKER = "ticker_gets_heads_up"; protected final NotificationMessagingUtil mMessagingUtil; protected final Context mContext; protected final HashMap mPendingNotifications = new HashMap<>(); protected final NotificationClicker mNotificationClicker = new NotificationClicker(); protected final ArraySet mHeadsUpEntriesToRemoveOnSwitch = new ArraySet<>(); // Dependencies: protected final NotificationLockscreenUserManager mLockscreenUserManager = Dependency.get(NotificationLockscreenUserManager.class); protected final NotificationGroupManager mGroupManager = Dependency.get(NotificationGroupManager.class); protected final NotificationGutsManager mGutsManager = Dependency.get(NotificationGutsManager.class); protected final NotificationRemoteInputManager mRemoteInputManager = Dependency.get(NotificationRemoteInputManager.class); protected final NotificationMediaManager mMediaManager = Dependency.get(NotificationMediaManager.class); protected final MetricsLogger mMetricsLogger = Dependency.get(MetricsLogger.class); protected final DeviceProvisionedController mDeviceProvisionedController = Dependency.get(DeviceProvisionedController.class); protected final VisualStabilityManager mVisualStabilityManager = Dependency.get(VisualStabilityManager.class); protected final UiOffloadThread mUiOffloadThread = Dependency.get(UiOffloadThread.class); protected final ForegroundServiceController mForegroundServiceController = Dependency.get(ForegroundServiceController.class); protected final NotificationListener mNotificationListener = Dependency.get(NotificationListener.class); private final SmartReplyController mSmartReplyController = Dependency.get(SmartReplyController.class); // A lifetime extender that watches for foreground service notifications private final NotificationLifetimeExtender mFGSExtender = new ForegroundServiceLifetimeExtender(); private final Map mRetainedNotifications = new ArrayMap<>(); protected IStatusBarService mBarService; protected NotificationPresenter mPresenter; protected Callback mCallback; protected PowerManager mPowerManager; protected SystemServicesProxy mSystemServicesProxy; protected NotificationListenerService.RankingMap mLatestRankingMap; protected HeadsUpManager mHeadsUpManager; protected NotificationData mNotificationData; protected ContentObserver mHeadsUpObserver; protected boolean mUseHeadsUp = false; protected boolean mDisableNotificationAlerts; protected NotificationListContainer mListContainer; private ExpandableNotificationRow.OnAppOpsClickListener mOnAppOpsClickListener; /** * Notifications with keys in this set are not actually around anymore. We kept them around * when they were canceled in response to a remote input interaction. This allows us to show * what you replied and allows you to continue typing into it. */ private final ArraySet mKeysKeptForRemoteInput = new ArraySet<>(); private final class NotificationClicker implements View.OnClickListener { @Override public void onClick(final View v) { if (!(v instanceof ExpandableNotificationRow)) { Log.e(TAG, "NotificationClicker called on a view that is not a notification row."); return; } mPresenter.wakeUpIfDozing(SystemClock.uptimeMillis(), v); final ExpandableNotificationRow row = (ExpandableNotificationRow) v; final StatusBarNotification sbn = row.getStatusBarNotification(); if (sbn == null) { Log.e(TAG, "NotificationClicker called on an unclickable notification,"); return; } // Check if the notification is displaying the menu, if so slide notification back if (row.getProvider() != null && row.getProvider().isMenuVisible()) { row.animateTranslateNotification(0); return; } // Mark notification for one frame. row.setJustClicked(true); DejankUtils.postAfterTraversal(() -> row.setJustClicked(false)); mCallback.onNotificationClicked(sbn, row); } public void register(ExpandableNotificationRow row, StatusBarNotification sbn) { Notification notification = sbn.getNotification(); if (notification.contentIntent != null || notification.fullScreenIntent != null) { row.setOnClickListener(this); } else { row.setOnClickListener(null); } } } private final DeviceProvisionedController.DeviceProvisionedListener mDeviceProvisionedListener = new DeviceProvisionedController.DeviceProvisionedListener() { @Override public void onDeviceProvisionedChanged() { updateNotifications(); } }; public NotificationListenerService.RankingMap getLatestRankingMap() { return mLatestRankingMap; } public void setLatestRankingMap(NotificationListenerService.RankingMap latestRankingMap) { mLatestRankingMap = latestRankingMap; } public void setDisableNotificationAlerts(boolean disableNotificationAlerts) { mDisableNotificationAlerts = disableNotificationAlerts; mHeadsUpObserver.onChange(true); } public void destroy() { mDeviceProvisionedController.removeCallback(mDeviceProvisionedListener); } public void onHeadsUpStateChanged(NotificationData.Entry entry, boolean isHeadsUp) { if (!isHeadsUp && mHeadsUpEntriesToRemoveOnSwitch.contains(entry)) { removeNotification(entry.key, getLatestRankingMap()); mHeadsUpEntriesToRemoveOnSwitch.remove(entry); if (mHeadsUpEntriesToRemoveOnSwitch.isEmpty()) { setLatestRankingMap(null); } } else { updateNotificationRanking(null); } } @Override public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { pw.println("NotificationEntryManager state:"); pw.print(" mPendingNotifications="); if (mPendingNotifications.size() == 0) { pw.println("null"); } else { for (NotificationData.Entry entry : mPendingNotifications.values()) { pw.println(entry.notification); } } pw.println(" Lifetime-extended notifications:"); if (mRetainedNotifications.isEmpty()) { pw.println(" None"); } else { for (Map.Entry entry : mRetainedNotifications.entrySet()) { pw.println(" " + entry.getKey().notification + " retained by " + entry.getValue().getClass().getName()); } } pw.print(" mUseHeadsUp="); pw.println(mUseHeadsUp); pw.print(" mKeysKeptForRemoteInput: "); pw.println(mKeysKeptForRemoteInput); } public NotificationEntryManager(Context context) { mContext = context; mPowerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); mBarService = IStatusBarService.Stub.asInterface( ServiceManager.getService(Context.STATUS_BAR_SERVICE)); mMessagingUtil = new NotificationMessagingUtil(context); mSystemServicesProxy = SystemServicesProxy.getInstance(mContext); mGroupManager.setPendingEntries(mPendingNotifications); mFGSExtender.setCallback(key -> removeNotification(key, mLatestRankingMap)); } public void setUpWithPresenter(NotificationPresenter presenter, NotificationListContainer listContainer, Callback callback, HeadsUpManager headsUpManager) { mPresenter = presenter; mCallback = callback; mNotificationData = new NotificationData(presenter); mHeadsUpManager = headsUpManager; mNotificationData.setHeadsUpManager(mHeadsUpManager); mListContainer = listContainer; mHeadsUpObserver = new ContentObserver(mPresenter.getHandler()) { @Override public void onChange(boolean selfChange) { boolean wasUsing = mUseHeadsUp; mUseHeadsUp = ENABLE_HEADS_UP && !mDisableNotificationAlerts && Settings.Global.HEADS_UP_OFF != Settings.Global.getInt( mContext.getContentResolver(), Settings.Global.HEADS_UP_NOTIFICATIONS_ENABLED, Settings.Global.HEADS_UP_OFF); Log.d(TAG, "heads up is " + (mUseHeadsUp ? "enabled" : "disabled")); if (wasUsing != mUseHeadsUp) { if (!mUseHeadsUp) { Log.d(TAG, "dismissing any existing heads up notification on disable event"); mHeadsUpManager.releaseAllImmediately(); } } } }; if (ENABLE_HEADS_UP) { mContext.getContentResolver().registerContentObserver( Settings.Global.getUriFor(Settings.Global.HEADS_UP_NOTIFICATIONS_ENABLED), true, mHeadsUpObserver); mContext.getContentResolver().registerContentObserver( Settings.Global.getUriFor(SETTING_HEADS_UP_TICKER), true, mHeadsUpObserver); } mDeviceProvisionedController.addCallback(mDeviceProvisionedListener); mHeadsUpObserver.onChange(true); // set up mOnAppOpsClickListener = mGutsManager::openGuts; } @VisibleForTesting protected Map getRetainedNotificationMap() { return mRetainedNotifications; } public NotificationData getNotificationData() { return mNotificationData; } public ExpandableNotificationRow.LongPressListener getNotificationLongClicker() { return mGutsManager::openGuts; } @Override public void logNotificationExpansion(String key, boolean userAction, boolean expanded) { mUiOffloadThread.submit(() -> { try { mBarService.onNotificationExpansionChanged(key, userAction, expanded); } catch (RemoteException e) { // Ignore. } }); } @Override public void onReorderingAllowed() { updateNotifications(); } private boolean shouldSuppressFullScreenIntent(NotificationData.Entry entry) { if (mPresenter.isDeviceInVrMode()) { return true; } return mNotificationData.shouldSuppressFullScreenIntent(entry); } private void inflateViews(NotificationData.Entry entry, ViewGroup parent) { PackageManager pmUser = StatusBar.getPackageManagerForUser(mContext, entry.notification.getUser().getIdentifier()); final StatusBarNotification sbn = entry.notification; if (entry.row != null) { entry.reset(); updateNotification(entry, pmUser, sbn, entry.row); } else { new RowInflaterTask().inflate(mContext, parent, entry, row -> { bindRow(entry, pmUser, sbn, row); updateNotification(entry, pmUser, sbn, row); }); } } private void bindRow(NotificationData.Entry entry, PackageManager pmUser, StatusBarNotification sbn, ExpandableNotificationRow row) { row.setExpansionLogger(this, entry.notification.getKey()); row.setGroupManager(mGroupManager); row.setHeadsUpManager(mHeadsUpManager); row.setOnExpandClickListener(mPresenter); row.setInflationCallback(this); row.setLongPressListener(getNotificationLongClicker()); mListContainer.bindRow(row); mRemoteInputManager.bindRow(row); // Get the app name. // Note that Notification.Builder#bindHeaderAppName has similar logic // but since this field is used in the guts, it must be accurate. // Therefore we will only show the application label, or, failing that, the // package name. No substitutions. final String pkg = sbn.getPackageName(); String appname = pkg; try { final ApplicationInfo info = pmUser.getApplicationInfo(pkg, PackageManager.MATCH_UNINSTALLED_PACKAGES | PackageManager.MATCH_DISABLED_COMPONENTS); if (info != null) { appname = String.valueOf(pmUser.getApplicationLabel(info)); } } catch (PackageManager.NameNotFoundException e) { // Do nothing } row.setAppName(appname); row.setOnDismissRunnable(() -> performRemoveNotification(row.getStatusBarNotification())); row.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS); if (ENABLE_REMOTE_INPUT) { row.setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS); } row.setAppOpsOnClickListener(mOnAppOpsClickListener); mCallback.onBindRow(entry, pmUser, sbn, row); } public void performRemoveNotification(StatusBarNotification n) { final int rank = mNotificationData.getRank(n.getKey()); final int count = mNotificationData.getActiveNotifications().size(); final NotificationVisibility nv = NotificationVisibility.obtain(n.getKey(), rank, count, true); NotificationData.Entry entry = mNotificationData.get(n.getKey()); if (FORCE_REMOTE_INPUT_HISTORY && mKeysKeptForRemoteInput.contains(n.getKey())) { mKeysKeptForRemoteInput.remove(n.getKey()); } mRemoteInputManager.onPerformRemoveNotification(n, entry); final String pkg = n.getPackageName(); final String tag = n.getTag(); final int id = n.getId(); final int userId = n.getUserId(); try { int dismissalSurface = NotificationStats.DISMISSAL_SHADE; if (isHeadsUp(n.getKey())) { dismissalSurface = NotificationStats.DISMISSAL_PEEK; } else if (mListContainer.hasPulsingNotifications()) { dismissalSurface = NotificationStats.DISMISSAL_AOD; } mBarService.onNotificationClear(pkg, tag, id, userId, n.getKey(), dismissalSurface, nv); removeNotification(n.getKey(), null); } catch (RemoteException ex) { // system process is dead if we're here. } mCallback.onPerformRemoveNotification(n); } /** * Cancel this notification and tell the StatusBarManagerService / NotificationManagerService * about the failure. * * WARNING: this will call back into us. Don't hold any locks. */ void handleNotificationError(StatusBarNotification n, String message) { removeNotification(n.getKey(), null); try { mBarService.onNotificationError(n.getPackageName(), n.getTag(), n.getId(), n.getUid(), n.getInitialPid(), message, n.getUserId()); } catch (RemoteException ex) { // The end is nigh. } } private void abortExistingInflation(String key) { if (mPendingNotifications.containsKey(key)) { NotificationData.Entry entry = mPendingNotifications.get(key); entry.abortTask(); mPendingNotifications.remove(key); } NotificationData.Entry addedEntry = mNotificationData.get(key); if (addedEntry != null) { addedEntry.abortTask(); } } @Override public void handleInflationException(StatusBarNotification notification, Exception e) { handleNotificationError(notification, e.getMessage()); } private void addEntry(NotificationData.Entry shadeEntry) { boolean isHeadsUped = shouldPeek(shadeEntry); if (isHeadsUped) { mHeadsUpManager.showNotification(shadeEntry); // Mark as seen immediately setNotificationShown(shadeEntry.notification); } addNotificationViews(shadeEntry); mCallback.onNotificationAdded(shadeEntry); } @Override public void onAsyncInflationFinished(NotificationData.Entry entry) { mPendingNotifications.remove(entry.key); // If there was an async task started after the removal, we don't want to add it back to // the list, otherwise we might get leaks. boolean isNew = mNotificationData.get(entry.key) == null; if (isNew && !entry.row.isRemoved()) { addEntry(entry); } else if (!isNew && entry.row.hasLowPriorityStateUpdated()) { mVisualStabilityManager.onLowPriorityUpdated(entry); mPresenter.updateNotificationViews(); } entry.row.setLowPriorityStateUpdated(false); } @Override public void removeNotification(String key, NotificationListenerService.RankingMap ranking) { // First chance to extend the lifetime of a notification NotificationData.Entry pendingEntry = mPendingNotifications.get(key); if (pendingEntry != null) { if (mFGSExtender.shouldExtendLifetimeForPendingNotification(pendingEntry)) { extendLifetime(pendingEntry, mFGSExtender); return; } } abortExistingInflation(key); boolean deferRemoval = false; if (mHeadsUpManager.isHeadsUp(key)) { // A cancel() in response to a remote input shouldn't be delayed, as it makes the // sending look longer than it takes. // Also we should not defer the removal if reordering isn't allowed since otherwise // some notifications can't disappear before the panel is closed. boolean ignoreEarliestRemovalTime = mRemoteInputManager.getController().isSpinning(key) && !FORCE_REMOTE_INPUT_HISTORY || !mVisualStabilityManager.isReorderingAllowed(); deferRemoval = !mHeadsUpManager.removeNotification(key, ignoreEarliestRemovalTime); } mMediaManager.onNotificationRemoved(key); NotificationData.Entry entry = mNotificationData.get(key); if (FORCE_REMOTE_INPUT_HISTORY && shouldKeepForRemoteInput(entry) && entry.row != null && !entry.row.isDismissed()) { CharSequence remoteInputText = entry.remoteInputText; if (TextUtils.isEmpty(remoteInputText)) { remoteInputText = entry.remoteInputTextWhenReset; } StatusBarNotification newSbn = rebuildNotificationWithRemoteInput(entry, remoteInputText, false /* showSpinner */); boolean updated = false; entry.onRemoteInputInserted(); try { updateNotificationInternal(newSbn, null); updated = true; } catch (InflationException e) { deferRemoval = false; } if (updated) { Log.w(TAG, "Keeping notification around after sending remote input "+ entry.key); addKeyKeptForRemoteInput(entry.key); return; } } if (FORCE_REMOTE_INPUT_HISTORY && shouldKeepForSmartReply(entry) && entry.row != null && !entry.row.isDismissed()) { // Turn off the spinner and hide buttons when an app cancels the notification. StatusBarNotification newSbn = rebuildNotificationForCanceledSmartReplies(entry); boolean updated = false; try { updateNotificationInternal(newSbn, null); updated = true; } catch (InflationException e) { // Ignore just don't keep the notification around. } // Treat the reply as longer sending. mSmartReplyController.stopSending(entry); if (updated) { Log.w(TAG, "Keeping notification around after sending smart reply " + entry.key); addKeyKeptForRemoteInput(entry.key); return; } } if (entry != null && mFGSExtender.shouldExtendLifetime(entry)) { extendLifetime(entry, mFGSExtender); return; } // Actually removing notification so smart reply controller can forget about it. mSmartReplyController.stopSending(entry); if (deferRemoval) { mLatestRankingMap = ranking; mHeadsUpEntriesToRemoveOnSwitch.add(mHeadsUpManager.getEntry(key)); return; } if (mRemoteInputManager.onRemoveNotification(entry)) { mLatestRankingMap = ranking; return; } if (entry != null && mGutsManager.getExposedGuts() != null && mGutsManager.getExposedGuts() == entry.row.getGuts() && entry.row.getGuts() != null && !entry.row.getGuts().isLeavebehind()) { Log.w(TAG, "Keeping notification because it's showing guts. " + key); mLatestRankingMap = ranking; mGutsManager.setKeyToRemoveOnGutsClosed(key); return; } if (entry != null) { mForegroundServiceController.removeNotification(entry.notification); } if (entry != null && entry.row != null) { entry.row.setRemoved(); mListContainer.cleanUpViewState(entry.row); } // Let's remove the children if this was a summary handleGroupSummaryRemoved(key); StatusBarNotification old = removeNotificationViews(key, ranking); // Make sure no lifetime extension is happening anymore cancelLifetimeExtension(entry); mCallback.onNotificationRemoved(key, old); } private void extendLifetime( NotificationData.Entry entry, NotificationLifetimeExtender extender) { // Cancel any other extender which might be holding on to this notification entry NotificationLifetimeExtender activeExtender = mRetainedNotifications.get(entry); if (activeExtender != null && activeExtender != extender) { activeExtender.setShouldManageLifetime(entry, false); } mRetainedNotifications.put(entry, extender); extender.setShouldManageLifetime(entry, true); } private void cancelLifetimeExtension(NotificationData.Entry entry) { NotificationLifetimeExtender activeExtender = mRetainedNotifications.remove(entry); if (activeExtender != null) { activeExtender.setShouldManageLifetime(entry, false); } } public StatusBarNotification rebuildNotificationWithRemoteInput(NotificationData.Entry entry, CharSequence remoteInputText, boolean showSpinner) { StatusBarNotification sbn = entry.notification; Notification.Builder b = Notification.Builder .recoverBuilder(mContext, sbn.getNotification().clone()); if (remoteInputText != null) { CharSequence[] oldHistory = sbn.getNotification().extras .getCharSequenceArray(Notification.EXTRA_REMOTE_INPUT_HISTORY); CharSequence[] newHistory; if (oldHistory == null) { newHistory = new CharSequence[1]; } else { newHistory = new CharSequence[oldHistory.length + 1]; System.arraycopy(oldHistory, 0, newHistory, 1, oldHistory.length); } newHistory[0] = String.valueOf(remoteInputText); b.setRemoteInputHistory(newHistory); } b.setShowRemoteInputSpinner(showSpinner); b.setHideSmartReplies(true); Notification newNotification = b.build(); // Undo any compatibility view inflation newNotification.contentView = sbn.getNotification().contentView; newNotification.bigContentView = sbn.getNotification().bigContentView; newNotification.headsUpContentView = sbn.getNotification().headsUpContentView; StatusBarNotification newSbn = new StatusBarNotification(sbn.getPackageName(), sbn.getOpPkg(), sbn.getId(), sbn.getTag(), sbn.getUid(), sbn.getInitialPid(), newNotification, sbn.getUser(), sbn.getOverrideGroupKey(), sbn.getPostTime()); return newSbn; } @VisibleForTesting StatusBarNotification rebuildNotificationForCanceledSmartReplies( NotificationData.Entry entry) { return rebuildNotificationWithRemoteInput(entry, null /* remoteInputTest */, false /* showSpinner */); } private boolean shouldKeepForSmartReply(NotificationData.Entry entry) { return entry != null && mSmartReplyController.isSendingSmartReply(entry.key); } private boolean shouldKeepForRemoteInput(NotificationData.Entry entry) { if (entry == null) { return false; } if (mRemoteInputManager.getController().isSpinning(entry.key)) { return true; } if (entry.hasJustSentRemoteInput()) { return true; } return false; } private StatusBarNotification removeNotificationViews(String key, NotificationListenerService.RankingMap ranking) { NotificationData.Entry entry = mNotificationData.remove(key, ranking); if (entry == null) { Log.w(TAG, "removeNotification for unknown key: " + key); return null; } updateNotifications(); Dependency.get(LeakDetector.class).trackGarbage(entry); return entry.notification; } /** * Ensures that the group children are cancelled immediately when the group summary is cancelled * instead of waiting for the notification manager to send all cancels. Otherwise this could * lead to flickers. * * This also ensures that the animation looks nice and only consists of a single disappear * animation instead of multiple. * @param key the key of the notification was removed * */ private void handleGroupSummaryRemoved(String key) { NotificationData.Entry entry = mNotificationData.get(key); if (entry != null && entry.row != null && entry.row.isSummaryWithChildren()) { if (entry.notification.getOverrideGroupKey() != null && !entry.row.isDismissed()) { // We don't want to remove children for autobundled notifications as they are not // always cancelled. We only remove them if they were dismissed by the user. return; } List notificationChildren = entry.row.getNotificationChildren(); for (int i = 0; i < notificationChildren.size(); i++) { ExpandableNotificationRow row = notificationChildren.get(i); if ((row.getStatusBarNotification().getNotification().flags & Notification.FLAG_FOREGROUND_SERVICE) != 0) { // the child is a foreground service notification which we can't remove! continue; } row.setKeepInParent(true); // we need to set this state earlier as otherwise we might generate some weird // animations row.setRemoved(); } } } public void updateNotificationsOnDensityOrFontScaleChanged() { ArrayList userNotifications = mNotificationData.getNotificationsForCurrentUser(); for (int i = 0; i < userNotifications.size(); i++) { NotificationData.Entry entry = userNotifications.get(i); boolean exposedGuts = mGutsManager.getExposedGuts() != null && entry.row.getGuts() == mGutsManager.getExposedGuts(); entry.row.onDensityOrFontScaleChanged(); if (exposedGuts) { mGutsManager.onDensityOrFontScaleChanged(entry.row); } } } protected void updateNotification(NotificationData.Entry entry, PackageManager pmUser, StatusBarNotification sbn, ExpandableNotificationRow row) { row.setNeedsRedaction(mLockscreenUserManager.needsRedaction(entry)); boolean isLowPriority = mNotificationData.isAmbient(sbn.getKey()); boolean isUpdate = mNotificationData.get(entry.key) != null; boolean wasLowPriority = row.isLowPriority(); row.setIsLowPriority(isLowPriority); row.setLowPriorityStateUpdated(isUpdate && (wasLowPriority != isLowPriority)); // bind the click event to the content area mNotificationClicker.register(row, sbn); // Extract target SDK version. try { ApplicationInfo info = pmUser.getApplicationInfo(sbn.getPackageName(), 0); entry.targetSdk = info.targetSdkVersion; } catch (PackageManager.NameNotFoundException ex) { Log.e(TAG, "Failed looking up ApplicationInfo for " + sbn.getPackageName(), ex); } row.setLegacy(entry.targetSdk >= Build.VERSION_CODES.GINGERBREAD && entry.targetSdk < Build.VERSION_CODES.LOLLIPOP); entry.setIconTag(R.id.icon_is_pre_L, entry.targetSdk < Build.VERSION_CODES.LOLLIPOP); entry.autoRedacted = entry.notification.getNotification().publicVersion == null; entry.row = row; entry.row.setOnActivatedListener(mPresenter); boolean useIncreasedCollapsedHeight = mMessagingUtil.isImportantMessaging(sbn, mNotificationData.getImportance(sbn.getKey())); boolean useIncreasedHeadsUp = useIncreasedCollapsedHeight && !mPresenter.isPresenterFullyCollapsed(); row.setUseIncreasedCollapsedHeight(useIncreasedCollapsedHeight); row.setUseIncreasedHeadsUpHeight(useIncreasedHeadsUp); row.updateNotification(entry); } protected void addNotificationViews(NotificationData.Entry entry) { if (entry == null) { return; } // Add the expanded view and icon. mNotificationData.add(entry); tagForeground(entry.notification); updateNotifications(); } protected NotificationData.Entry createNotificationViews(StatusBarNotification sbn) throws InflationException { if (DEBUG) { Log.d(TAG, "createNotificationViews(notification=" + sbn); } NotificationData.Entry entry = new NotificationData.Entry(sbn); Dependency.get(LeakDetector.class).trackInstance(entry); entry.createIcons(mContext, sbn); // Construct the expanded view. inflateViews(entry, mListContainer.getViewParentForNotification(entry)); return entry; } private void addNotificationInternal(StatusBarNotification notification, NotificationListenerService.RankingMap ranking) throws InflationException { String key = notification.getKey(); if (DEBUG) Log.d(TAG, "addNotification key=" + key); mNotificationData.updateRanking(ranking); NotificationData.Entry shadeEntry = createNotificationViews(notification); boolean isHeadsUped = shouldPeek(shadeEntry); if (!isHeadsUped && notification.getNotification().fullScreenIntent != null) { if (shouldSuppressFullScreenIntent(shadeEntry)) { if (DEBUG) { Log.d(TAG, "No Fullscreen intent: suppressed by DND: " + key); } } else if (mNotificationData.getImportance(key) < NotificationManager.IMPORTANCE_HIGH) { if (DEBUG) { Log.d(TAG, "No Fullscreen intent: not important enough: " + key); } } else { // Stop screensaver if the notification has a fullscreen intent. // (like an incoming phone call) SystemServicesProxy.getInstance(mContext).awakenDreamsAsync(); // not immersive & a fullscreen alert should be shown if (DEBUG) Log.d(TAG, "Notification has fullScreenIntent; sending fullScreenIntent"); try { EventLog.writeEvent(EventLogTags.SYSUI_FULLSCREEN_NOTIFICATION, key); notification.getNotification().fullScreenIntent.send(); shadeEntry.notifyFullScreenIntentLaunched(); mMetricsLogger.count("note_fullscreen", 1); } catch (PendingIntent.CanceledException e) { } } } abortExistingInflation(key); mForegroundServiceController.addNotification(notification, mNotificationData.getImportance(key)); mPendingNotifications.put(key, shadeEntry); mGroupManager.onPendingEntryAdded(shadeEntry); } @VisibleForTesting protected void tagForeground(StatusBarNotification notification) { ArraySet activeOps = mForegroundServiceController.getAppOps( notification.getUserId(), notification.getPackageName()); if (activeOps != null) { int N = activeOps.size(); for (int i = 0; i < N; i++) { updateNotificationsForAppOp(activeOps.valueAt(i), notification.getUid(), notification.getPackageName(), true); } } } @Override public void addNotification(StatusBarNotification notification, NotificationListenerService.RankingMap ranking) { try { addNotificationInternal(notification, ranking); } catch (InflationException e) { handleInflationException(notification, e); } } public void updateNotificationsForAppOp(int appOp, int uid, String pkg, boolean showIcon) { String foregroundKey = mForegroundServiceController.getStandardLayoutKey( UserHandle.getUserId(uid), pkg); if (foregroundKey != null) { mNotificationData.updateAppOp(appOp, uid, pkg, foregroundKey, showIcon); updateNotifications(); } } private boolean alertAgain(NotificationData.Entry oldEntry, Notification newNotification) { return oldEntry == null || !oldEntry.hasInterrupted() || (newNotification.flags & Notification.FLAG_ONLY_ALERT_ONCE) == 0; } private void updateNotificationInternal(StatusBarNotification notification, NotificationListenerService.RankingMap ranking) throws InflationException { if (DEBUG) Log.d(TAG, "updateNotification(" + notification + ")"); final String key = notification.getKey(); abortExistingInflation(key); NotificationData.Entry entry = mNotificationData.get(key); if (entry == null) { return; } mHeadsUpEntriesToRemoveOnSwitch.remove(entry); mRemoteInputManager.onUpdateNotification(entry); mSmartReplyController.stopSending(entry); if (key.equals(mGutsManager.getKeyToRemoveOnGutsClosed())) { mGutsManager.setKeyToRemoveOnGutsClosed(null); Log.w(TAG, "Notification that was kept for guts was updated. " + key); } // No need to keep the lifetime extension around if an update comes in cancelLifetimeExtension(entry); Notification n = notification.getNotification(); mNotificationData.updateRanking(ranking); final StatusBarNotification oldNotification = entry.notification; entry.notification = notification; mGroupManager.onEntryUpdated(entry, oldNotification); entry.updateIcons(mContext, notification); inflateViews(entry, mListContainer.getViewParentForNotification(entry)); mForegroundServiceController.updateNotification(notification, mNotificationData.getImportance(key)); boolean shouldPeek = shouldPeek(entry, notification); boolean alertAgain = alertAgain(entry, n); updateHeadsUp(key, entry, shouldPeek, alertAgain); updateNotifications(); if (!notification.isClearable()) { // The user may have performed a dismiss action on the notification, since it's // not clearable we should snap it back. mListContainer.snapViewIfNeeded(entry.row); } if (DEBUG) { // Is this for you? boolean isForCurrentUser = mPresenter.isNotificationForCurrentProfiles(notification); Log.d(TAG, "notification is " + (isForCurrentUser ? "" : "not ") + "for you"); } mCallback.onNotificationUpdated(notification); } @Override public void updateNotification(StatusBarNotification notification, NotificationListenerService.RankingMap ranking) { try { updateNotificationInternal(notification, ranking); } catch (InflationException e) { handleInflationException(notification, e); } } public void updateNotifications() { mNotificationData.filterAndSort(); mPresenter.updateNotificationViews(); } public void updateNotificationRanking(NotificationListenerService.RankingMap ranking) { mNotificationData.updateRanking(ranking); updateNotifications(); } protected boolean shouldPeek(NotificationData.Entry entry) { return shouldPeek(entry, entry.notification); } public boolean shouldPeek(NotificationData.Entry entry, StatusBarNotification sbn) { if (!mUseHeadsUp || mPresenter.isDeviceInVrMode()) { if (DEBUG) Log.d(TAG, "No peeking: no huns or vr mode"); return false; } if (mNotificationData.shouldFilterOut(entry)) { if (DEBUG) Log.d(TAG, "No peeking: filtered notification: " + sbn.getKey()); return false; } boolean inUse = mPowerManager.isScreenOn() && !mSystemServicesProxy.isDreaming(); if (!inUse && !mPresenter.isDozing()) { if (DEBUG) { Log.d(TAG, "No peeking: not in use: " + sbn.getKey()); } return false; } if (!mPresenter.isDozing() && mNotificationData.shouldSuppressPeek(entry)) { if (DEBUG) Log.d(TAG, "No peeking: suppressed by DND: " + sbn.getKey()); return false; } // Peeking triggers an ambient display pulse, so disable peek is ambient is active if (mPresenter.isDozing() && mNotificationData.shouldSuppressAmbient(entry)) { if (DEBUG) Log.d(TAG, "No peeking: suppressed by DND: " + sbn.getKey()); return false; } if (entry.hasJustLaunchedFullScreenIntent()) { if (DEBUG) Log.d(TAG, "No peeking: recent fullscreen: " + sbn.getKey()); return false; } if (isSnoozedPackage(sbn)) { if (DEBUG) Log.d(TAG, "No peeking: snoozed package: " + sbn.getKey()); return false; } // Allow peeking for DEFAULT notifications only if we're on Ambient Display. int importanceLevel = mPresenter.isDozing() ? NotificationManager.IMPORTANCE_DEFAULT : NotificationManager.IMPORTANCE_HIGH; if (mNotificationData.getImportance(sbn.getKey()) < importanceLevel) { if (DEBUG) Log.d(TAG, "No peeking: unimportant notification: " + sbn.getKey()); return false; } // Don't peek notifications that are suppressed due to group alert behavior if (sbn.isGroup() && sbn.getNotification().suppressAlertingDueToGrouping()) { if (DEBUG) Log.d(TAG, "No peeking: suppressed due to group alert behavior"); return false; } if (!mCallback.shouldPeek(entry, sbn)) { return false; } return true; } protected void setNotificationShown(StatusBarNotification n) { setNotificationsShown(new String[]{n.getKey()}); } protected void setNotificationsShown(String[] keys) { try { mNotificationListener.setNotificationsShown(keys); } catch (RuntimeException e) { Log.d(TAG, "failed setNotificationsShown: ", e); } } protected boolean isSnoozedPackage(StatusBarNotification sbn) { return mHeadsUpManager.isSnoozed(sbn.getPackageName()); } protected void updateHeadsUp(String key, NotificationData.Entry entry, boolean shouldPeek, boolean alertAgain) { final boolean wasHeadsUp = isHeadsUp(key); if (wasHeadsUp) { if (!shouldPeek) { // We don't want this to be interrupting anymore, lets remove it mHeadsUpManager.removeNotification(key, false /* ignoreEarliestRemovalTime */); } else { mHeadsUpManager.updateNotification(entry, alertAgain); } } else if (shouldPeek && alertAgain) { // This notification was updated to be a heads-up, show it! mHeadsUpManager.showNotification(entry); } } protected boolean isHeadsUp(String key) { return mHeadsUpManager.isHeadsUp(key); } public boolean isNotificationKeptForRemoteInput(String key) { return mKeysKeptForRemoteInput.contains(key); } public void removeKeyKeptForRemoteInput(String key) { mKeysKeptForRemoteInput.remove(key); } public void addKeyKeptForRemoteInput(String key) { if (FORCE_REMOTE_INPUT_HISTORY) { mKeysKeptForRemoteInput.add(key); } } /** * Callback for NotificationEntryManager. */ public interface Callback { /** * Called when a new entry is created. * * @param shadeEntry entry that was created */ void onNotificationAdded(NotificationData.Entry shadeEntry); /** * Called when a notification was updated. * * @param notification notification that was updated */ void onNotificationUpdated(StatusBarNotification notification); /** * Called when a notification was removed. * * @param key key of notification that was removed * @param old StatusBarNotification of the notification before it was removed */ void onNotificationRemoved(String key, StatusBarNotification old); /** * Called when a notification is clicked. * * @param sbn notification that was clicked * @param row row for that notification */ void onNotificationClicked(StatusBarNotification sbn, ExpandableNotificationRow row); /** * Called when a new notification and row is created. * * @param entry entry for the notification * @param pmUser package manager for user * @param sbn notification * @param row row for the notification */ void onBindRow(NotificationData.Entry entry, PackageManager pmUser, StatusBarNotification sbn, ExpandableNotificationRow row); /** * Removes a notification immediately. * * @param statusBarNotification notification that is being removed */ void onPerformRemoveNotification(StatusBarNotification statusBarNotification); /** * Returns true if NotificationEntryManager should peek this notification. * * @param entry entry of the notification that might be peeked * @param sbn notification that might be peeked * @return true if the notification should be peeked */ boolean shouldPeek(NotificationData.Entry entry, StatusBarNotification sbn); } }