/* * Copyright (C) 2018 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 android.annotation.NonNull; import android.annotation.Nullable; import android.os.Handler; import android.os.Looper; import android.os.SystemClock; import android.util.ArrayMap; import android.util.ArraySet; import android.util.Log; import android.view.accessibility.AccessibilityEvent; import com.android.internal.annotations.VisibleForTesting; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.row.NotificationContentInflater.InflationFlag; import java.util.stream.Stream; /** * A manager which contains notification alerting functionality, providing methods to add and * remove notifications that appear on screen for a period of time and dismiss themselves at the * appropriate time. These include heads up notifications and ambient pulses. */ public abstract class AlertingNotificationManager implements NotificationLifetimeExtender { private static final String TAG = "AlertNotifManager"; protected final Clock mClock = new Clock(); protected final ArrayMap mAlertEntries = new ArrayMap<>(); /** * This is the list of entries that have already been removed from the * NotificationManagerService side, but we keep it to prevent the UI from looking weird and * will remove when possible. See {@link NotificationLifetimeExtender} */ protected final ArraySet mExtendedLifetimeAlertEntries = new ArraySet<>(); protected NotificationSafeToRemoveCallback mNotificationLifetimeFinishedCallback; protected int mMinimumDisplayTime; protected int mAutoDismissNotificationDecay; @VisibleForTesting public Handler mHandler = new Handler(Looper.getMainLooper()); /** * Called when posting a new notification that should alert the user and appear on screen. * Adds the notification to be managed. * @param entry entry to show */ public void showNotification(@NonNull NotificationEntry entry) { if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "showNotification"); } addAlertEntry(entry); updateNotification(entry.key, true /* alert */); entry.setInterruption(); } /** * Try to remove the notification. May not succeed if the notification has not been shown long * enough and needs to be kept around. * @param key the key of the notification to remove * @param releaseImmediately force a remove regardless of earliest removal time * @return true if notification is removed, false otherwise */ public boolean removeNotification(@NonNull String key, boolean releaseImmediately) { if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "removeNotification"); } AlertEntry alertEntry = mAlertEntries.get(key); if (alertEntry == null) { return true; } if (releaseImmediately || canRemoveImmediately(key)) { removeAlertEntry(key); } else { alertEntry.removeAsSoonAsPossible(); return false; } return true; } /** * Called when the notification state has been updated. * @param key the key of the entry that was updated * @param alert whether the notification should alert again and force reevaluation of * removal time */ public void updateNotification(@NonNull String key, boolean alert) { if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "updateNotification"); } AlertEntry alertEntry = mAlertEntries.get(key); if (alertEntry == null) { // the entry was released before this update (i.e by a listener) This can happen // with the groupmanager return; } alertEntry.mEntry.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); if (alert) { alertEntry.updateEntry(true /* updatePostTime */); } } /** * Clears all managed notifications. */ public void releaseAllImmediately() { if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "releaseAllImmediately"); } // A copy is necessary here as we are changing the underlying map. This would cause // undefined behavior if we iterated over the key set directly. ArraySet keysToRemove = new ArraySet<>(mAlertEntries.keySet()); for (String key : keysToRemove) { removeAlertEntry(key); } } /** * Returns the entry if it is managed by this manager. * @param key key of notification * @return the entry */ @Nullable public NotificationEntry getEntry(@NonNull String key) { AlertEntry entry = mAlertEntries.get(key); return entry != null ? entry.mEntry : null; } /** * Returns the stream of all current notifications managed by this manager. * @return all entries */ @NonNull public Stream getAllEntries() { return mAlertEntries.values().stream().map(headsUpEntry -> headsUpEntry.mEntry); } /** * Whether or not there are any active alerting notifications. * @return true if there is an alert, false otherwise */ public boolean hasNotifications() { return !mAlertEntries.isEmpty(); } /** * Whether or not the given notification is alerting and managed by this manager. * @return true if the notification is alerting */ public boolean isAlerting(@NonNull String key) { return mAlertEntries.containsKey(key); } /** * Gets the flag corresponding to the notification content view this alert manager will show. * * @return flag corresponding to the content view */ public abstract @InflationFlag int getContentFlag(); /** * Add a new entry and begin managing it. * @param entry the entry to add */ protected final void addAlertEntry(@NonNull NotificationEntry entry) { AlertEntry alertEntry = createAlertEntry(); alertEntry.setEntry(entry); mAlertEntries.put(entry.key, alertEntry); onAlertEntryAdded(alertEntry); entry.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); } /** * Manager-specific logic that should occur when an entry is added. * @param alertEntry alert entry added */ protected abstract void onAlertEntryAdded(@NonNull AlertEntry alertEntry); /** * Remove a notification and reset the alert entry. * @param key key of notification to remove */ protected final void removeAlertEntry(@NonNull String key) { AlertEntry alertEntry = mAlertEntries.get(key); if (alertEntry == null) { return; } NotificationEntry entry = alertEntry.mEntry; mAlertEntries.remove(key); onAlertEntryRemoved(alertEntry); entry.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); alertEntry.reset(); if (mExtendedLifetimeAlertEntries.contains(entry)) { if (mNotificationLifetimeFinishedCallback != null) { mNotificationLifetimeFinishedCallback.onSafeToRemove(key); } mExtendedLifetimeAlertEntries.remove(entry); } } /** * Manager-specific logic that should occur when an alert entry is removed. * @param alertEntry alert entry removed */ protected abstract void onAlertEntryRemoved(@NonNull AlertEntry alertEntry); /** * Returns a new alert entry instance. * @return a new AlertEntry */ protected AlertEntry createAlertEntry() { return new AlertEntry(); } /** * Whether or not the alert can be removed currently. If it hasn't been on screen long enough * it should not be removed unless forced * @param key the key to check if removable * @return true if the alert entry can be removed */ protected boolean canRemoveImmediately(String key) { AlertEntry alertEntry = mAlertEntries.get(key); return alertEntry == null || alertEntry.wasShownLongEnough() || alertEntry.mEntry.isRowDismissed(); } /////////////////////////////////////////////////////////////////////////////////////////////// // NotificationLifetimeExtender Methods @Override public void setCallback(NotificationSafeToRemoveCallback callback) { mNotificationLifetimeFinishedCallback = callback; } @Override public boolean shouldExtendLifetime(NotificationEntry entry) { return !canRemoveImmediately(entry.key); } @Override public void setShouldManageLifetime(NotificationEntry entry, boolean shouldExtend) { if (shouldExtend) { mExtendedLifetimeAlertEntries.add(entry); // We need to make sure that entries are stopping to alert eventually, let's remove // this as soon as possible. AlertEntry alertEntry = mAlertEntries.get(entry.key); alertEntry.removeAsSoonAsPossible(); } else { mExtendedLifetimeAlertEntries.remove(entry); } } /////////////////////////////////////////////////////////////////////////////////////////////// protected class AlertEntry implements Comparable { @Nullable public NotificationEntry mEntry; public long mPostTime; public long mEarliestRemovaltime; @Nullable protected Runnable mRemoveAlertRunnable; public void setEntry(@NonNull final NotificationEntry entry) { setEntry(entry, () -> removeAlertEntry(entry.key)); } public void setEntry(@NonNull final NotificationEntry entry, @Nullable Runnable removeAlertRunnable) { mEntry = entry; mRemoveAlertRunnable = removeAlertRunnable; mPostTime = calculatePostTime(); updateEntry(true /* updatePostTime */); } /** * Updates an entry's removal time. * @param updatePostTime whether or not to refresh the post time */ public void updateEntry(boolean updatePostTime) { if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "updateEntry"); } long currentTime = mClock.currentTimeMillis(); mEarliestRemovaltime = currentTime + mMinimumDisplayTime; if (updatePostTime) { mPostTime = Math.max(mPostTime, currentTime); } removeAutoRemovalCallbacks(); if (!isSticky()) { long finishTime = calculateFinishTime(); long removeDelay = Math.max(finishTime - currentTime, mMinimumDisplayTime); mHandler.postDelayed(mRemoveAlertRunnable, removeDelay); } } /** * Whether or not the notification is "sticky" i.e. should stay on screen regardless * of the timer and should be removed externally. * @return true if the notification is sticky */ protected boolean isSticky() { return false; } /** * Whether the notification has been on screen long enough and can be removed. * @return true if the notification has been on screen long enough */ public boolean wasShownLongEnough() { return mEarliestRemovaltime < mClock.currentTimeMillis(); } @Override public int compareTo(@NonNull AlertEntry alertEntry) { return (mPostTime < alertEntry.mPostTime) ? 1 : ((mPostTime == alertEntry.mPostTime) ? mEntry.key.compareTo(alertEntry.mEntry.key) : -1); } public void reset() { mEntry = null; removeAutoRemovalCallbacks(); mRemoveAlertRunnable = null; } /** * Clear any pending removal runnables. */ public void removeAutoRemovalCallbacks() { if (mRemoveAlertRunnable != null) { mHandler.removeCallbacks(mRemoveAlertRunnable); } } /** * Remove the alert at the earliest allowed removal time. */ public void removeAsSoonAsPossible() { if (mRemoveAlertRunnable != null) { removeAutoRemovalCallbacks(); mHandler.postDelayed(mRemoveAlertRunnable, mEarliestRemovaltime - mClock.currentTimeMillis()); } } /** * Calculate what the post time of a notification is at some current time. * @return the post time */ protected long calculatePostTime() { return mClock.currentTimeMillis(); } /** * Calculate when the notification should auto-dismiss itself. * @return the finish time */ protected long calculateFinishTime() { return mPostTime + mAutoDismissNotificationDecay; } } protected final static class Clock { public long currentTimeMillis() { return SystemClock.elapsedRealtime(); } } }