| /* |
| * 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<String, AlertEntry> 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<NotificationEntry> 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<String> 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<NotificationEntry> 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(); |
| } |
| |
| /////////////////////////////////////////////////////////////////////////////////////////////// |
| // 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<AlertEntry> { |
| @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(); |
| } |
| } |
| } |