Home | History | Annotate | Download | only in statusbar
      1 /*
      2  * Copyright (C) 2018 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.systemui.statusbar;
     18 
     19 import android.annotation.NonNull;
     20 import android.annotation.Nullable;
     21 import android.os.Handler;
     22 import android.os.Looper;
     23 import android.os.SystemClock;
     24 import android.util.ArrayMap;
     25 import android.util.ArraySet;
     26 import android.util.Log;
     27 import android.view.accessibility.AccessibilityEvent;
     28 
     29 import com.android.internal.annotations.VisibleForTesting;
     30 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
     31 import com.android.systemui.statusbar.notification.row.NotificationContentInflater.InflationFlag;
     32 
     33 import java.util.stream.Stream;
     34 
     35 /**
     36  * A manager which contains notification alerting functionality, providing methods to add and
     37  * remove notifications that appear on screen for a period of time and dismiss themselves at the
     38  * appropriate time.  These include heads up notifications and ambient pulses.
     39  */
     40 public abstract class AlertingNotificationManager implements NotificationLifetimeExtender {
     41     private static final String TAG = "AlertNotifManager";
     42     protected final Clock mClock = new Clock();
     43     protected final ArrayMap<String, AlertEntry> mAlertEntries = new ArrayMap<>();
     44 
     45     /**
     46      * This is the list of entries that have already been removed from the
     47      * NotificationManagerService side, but we keep it to prevent the UI from looking weird and
     48      * will remove when possible. See {@link NotificationLifetimeExtender}
     49      */
     50     protected final ArraySet<NotificationEntry> mExtendedLifetimeAlertEntries = new ArraySet<>();
     51 
     52     protected NotificationSafeToRemoveCallback mNotificationLifetimeFinishedCallback;
     53     protected int mMinimumDisplayTime;
     54     protected int mAutoDismissNotificationDecay;
     55     @VisibleForTesting
     56     public Handler mHandler = new Handler(Looper.getMainLooper());
     57 
     58     /**
     59      * Called when posting a new notification that should alert the user and appear on screen.
     60      * Adds the notification to be managed.
     61      * @param entry entry to show
     62      */
     63     public void showNotification(@NonNull NotificationEntry entry) {
     64         if (Log.isLoggable(TAG, Log.VERBOSE)) {
     65             Log.v(TAG, "showNotification");
     66         }
     67         addAlertEntry(entry);
     68         updateNotification(entry.key, true /* alert */);
     69         entry.setInterruption();
     70     }
     71 
     72     /**
     73      * Try to remove the notification.  May not succeed if the notification has not been shown long
     74      * enough and needs to be kept around.
     75      * @param key the key of the notification to remove
     76      * @param releaseImmediately force a remove regardless of earliest removal time
     77      * @return true if notification is removed, false otherwise
     78      */
     79     public boolean removeNotification(@NonNull String key, boolean releaseImmediately) {
     80         if (Log.isLoggable(TAG, Log.VERBOSE)) {
     81             Log.v(TAG, "removeNotification");
     82         }
     83         AlertEntry alertEntry = mAlertEntries.get(key);
     84         if (alertEntry == null) {
     85             return true;
     86         }
     87         if (releaseImmediately || canRemoveImmediately(key)) {
     88             removeAlertEntry(key);
     89         } else {
     90             alertEntry.removeAsSoonAsPossible();
     91             return false;
     92         }
     93         return true;
     94     }
     95 
     96     /**
     97      * Called when the notification state has been updated.
     98      * @param key the key of the entry that was updated
     99      * @param alert whether the notification should alert again and force reevaluation of
    100      *              removal time
    101      */
    102     public void updateNotification(@NonNull String key, boolean alert) {
    103         if (Log.isLoggable(TAG, Log.VERBOSE)) {
    104             Log.v(TAG, "updateNotification");
    105         }
    106 
    107         AlertEntry alertEntry = mAlertEntries.get(key);
    108         if (alertEntry == null) {
    109             // the entry was released before this update (i.e by a listener) This can happen
    110             // with the groupmanager
    111             return;
    112         }
    113 
    114         alertEntry.mEntry.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
    115         if (alert) {
    116             alertEntry.updateEntry(true /* updatePostTime */);
    117         }
    118     }
    119 
    120     /**
    121      * Clears all managed notifications.
    122      */
    123     public void releaseAllImmediately() {
    124         if (Log.isLoggable(TAG, Log.VERBOSE)) {
    125             Log.v(TAG, "releaseAllImmediately");
    126         }
    127         // A copy is necessary here as we are changing the underlying map.  This would cause
    128         // undefined behavior if we iterated over the key set directly.
    129         ArraySet<String> keysToRemove = new ArraySet<>(mAlertEntries.keySet());
    130         for (String key : keysToRemove) {
    131             removeAlertEntry(key);
    132         }
    133     }
    134 
    135     /**
    136      * Returns the entry if it is managed by this manager.
    137      * @param key key of notification
    138      * @return the entry
    139      */
    140     @Nullable
    141     public NotificationEntry getEntry(@NonNull String key) {
    142         AlertEntry entry = mAlertEntries.get(key);
    143         return entry != null ? entry.mEntry : null;
    144     }
    145 
    146     /**
    147      * Returns the stream of all current notifications managed by this manager.
    148      * @return all entries
    149      */
    150     @NonNull
    151     public Stream<NotificationEntry> getAllEntries() {
    152         return mAlertEntries.values().stream().map(headsUpEntry -> headsUpEntry.mEntry);
    153     }
    154 
    155     /**
    156      * Whether or not there are any active alerting notifications.
    157      * @return true if there is an alert, false otherwise
    158      */
    159     public boolean hasNotifications() {
    160         return !mAlertEntries.isEmpty();
    161     }
    162 
    163     /**
    164      * Whether or not the given notification is alerting and managed by this manager.
    165      * @return true if the notification is alerting
    166      */
    167     public boolean isAlerting(@NonNull String key) {
    168         return mAlertEntries.containsKey(key);
    169     }
    170 
    171     /**
    172      * Gets the flag corresponding to the notification content view this alert manager will show.
    173      *
    174      * @return flag corresponding to the content view
    175      */
    176     public abstract @InflationFlag int getContentFlag();
    177 
    178     /**
    179      * Add a new entry and begin managing it.
    180      * @param entry the entry to add
    181      */
    182     protected final void addAlertEntry(@NonNull NotificationEntry entry) {
    183         AlertEntry alertEntry = createAlertEntry();
    184         alertEntry.setEntry(entry);
    185         mAlertEntries.put(entry.key, alertEntry);
    186         onAlertEntryAdded(alertEntry);
    187         entry.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
    188     }
    189 
    190     /**
    191      * Manager-specific logic that should occur when an entry is added.
    192      * @param alertEntry alert entry added
    193      */
    194     protected abstract void onAlertEntryAdded(@NonNull AlertEntry alertEntry);
    195 
    196     /**
    197      * Remove a notification and reset the alert entry.
    198      * @param key key of notification to remove
    199      */
    200     protected final void removeAlertEntry(@NonNull String key) {
    201         AlertEntry alertEntry = mAlertEntries.get(key);
    202         if (alertEntry == null) {
    203             return;
    204         }
    205         NotificationEntry entry = alertEntry.mEntry;
    206         mAlertEntries.remove(key);
    207         onAlertEntryRemoved(alertEntry);
    208         entry.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
    209         alertEntry.reset();
    210         if (mExtendedLifetimeAlertEntries.contains(entry)) {
    211             if (mNotificationLifetimeFinishedCallback != null) {
    212                 mNotificationLifetimeFinishedCallback.onSafeToRemove(key);
    213             }
    214             mExtendedLifetimeAlertEntries.remove(entry);
    215         }
    216     }
    217 
    218     /**
    219      * Manager-specific logic that should occur when an alert entry is removed.
    220      * @param alertEntry alert entry removed
    221      */
    222     protected abstract void onAlertEntryRemoved(@NonNull AlertEntry alertEntry);
    223 
    224     /**
    225      * Returns a new alert entry instance.
    226      * @return a new AlertEntry
    227      */
    228     protected AlertEntry createAlertEntry() {
    229         return new AlertEntry();
    230     }
    231 
    232     /**
    233      * Whether or not the alert can be removed currently.  If it hasn't been on screen long enough
    234      * it should not be removed unless forced
    235      * @param key the key to check if removable
    236      * @return true if the alert entry can be removed
    237      */
    238     protected boolean canRemoveImmediately(String key) {
    239         AlertEntry alertEntry = mAlertEntries.get(key);
    240         return alertEntry == null || alertEntry.wasShownLongEnough()
    241                 || alertEntry.mEntry.isRowDismissed();
    242     }
    243 
    244     ///////////////////////////////////////////////////////////////////////////////////////////////
    245     // NotificationLifetimeExtender Methods
    246 
    247     @Override
    248     public void setCallback(NotificationSafeToRemoveCallback callback) {
    249         mNotificationLifetimeFinishedCallback = callback;
    250     }
    251 
    252     @Override
    253     public boolean shouldExtendLifetime(NotificationEntry entry) {
    254         return !canRemoveImmediately(entry.key);
    255     }
    256 
    257     @Override
    258     public void setShouldManageLifetime(NotificationEntry entry, boolean shouldExtend) {
    259         if (shouldExtend) {
    260             mExtendedLifetimeAlertEntries.add(entry);
    261             // We need to make sure that entries are stopping to alert eventually, let's remove
    262             // this as soon as possible.
    263             AlertEntry alertEntry = mAlertEntries.get(entry.key);
    264             alertEntry.removeAsSoonAsPossible();
    265         } else {
    266             mExtendedLifetimeAlertEntries.remove(entry);
    267         }
    268     }
    269     ///////////////////////////////////////////////////////////////////////////////////////////////
    270 
    271     protected class AlertEntry implements Comparable<AlertEntry> {
    272         @Nullable public NotificationEntry mEntry;
    273         public long mPostTime;
    274         public long mEarliestRemovaltime;
    275 
    276         @Nullable protected Runnable mRemoveAlertRunnable;
    277 
    278         public void setEntry(@NonNull final NotificationEntry entry) {
    279             setEntry(entry, () -> removeAlertEntry(entry.key));
    280         }
    281 
    282         public void setEntry(@NonNull final NotificationEntry entry,
    283                 @Nullable Runnable removeAlertRunnable) {
    284             mEntry = entry;
    285             mRemoveAlertRunnable = removeAlertRunnable;
    286 
    287             mPostTime = calculatePostTime();
    288             updateEntry(true /* updatePostTime */);
    289         }
    290 
    291         /**
    292          * Updates an entry's removal time.
    293          * @param updatePostTime whether or not to refresh the post time
    294          */
    295         public void updateEntry(boolean updatePostTime) {
    296             if (Log.isLoggable(TAG, Log.VERBOSE)) {
    297                 Log.v(TAG, "updateEntry");
    298             }
    299 
    300             long currentTime = mClock.currentTimeMillis();
    301             mEarliestRemovaltime = currentTime + mMinimumDisplayTime;
    302             if (updatePostTime) {
    303                 mPostTime = Math.max(mPostTime, currentTime);
    304             }
    305             removeAutoRemovalCallbacks();
    306 
    307             if (!isSticky()) {
    308                 long finishTime = calculateFinishTime();
    309                 long removeDelay = Math.max(finishTime - currentTime, mMinimumDisplayTime);
    310                 mHandler.postDelayed(mRemoveAlertRunnable, removeDelay);
    311             }
    312         }
    313 
    314         /**
    315          * Whether or not the notification is "sticky" i.e. should stay on screen regardless
    316          * of the timer and should be removed externally.
    317          * @return true if the notification is sticky
    318          */
    319         protected boolean isSticky() {
    320             return false;
    321         }
    322 
    323         /**
    324          * Whether the notification has been on screen long enough and can be removed.
    325          * @return true if the notification has been on screen long enough
    326          */
    327         public boolean wasShownLongEnough() {
    328             return mEarliestRemovaltime < mClock.currentTimeMillis();
    329         }
    330 
    331         @Override
    332         public int compareTo(@NonNull AlertEntry alertEntry) {
    333             return (mPostTime < alertEntry.mPostTime)
    334                     ? 1 : ((mPostTime == alertEntry.mPostTime)
    335                             ? mEntry.key.compareTo(alertEntry.mEntry.key) : -1);
    336         }
    337 
    338         public void reset() {
    339             mEntry = null;
    340             removeAutoRemovalCallbacks();
    341             mRemoveAlertRunnable = null;
    342         }
    343 
    344         /**
    345          * Clear any pending removal runnables.
    346          */
    347         public void removeAutoRemovalCallbacks() {
    348             if (mRemoveAlertRunnable != null) {
    349                 mHandler.removeCallbacks(mRemoveAlertRunnable);
    350             }
    351         }
    352 
    353         /**
    354          * Remove the alert at the earliest allowed removal time.
    355          */
    356         public void removeAsSoonAsPossible() {
    357             if (mRemoveAlertRunnable != null) {
    358                 removeAutoRemovalCallbacks();
    359                 mHandler.postDelayed(mRemoveAlertRunnable,
    360                         mEarliestRemovaltime - mClock.currentTimeMillis());
    361             }
    362         }
    363 
    364         /**
    365          * Calculate what the post time of a notification is at some current time.
    366          * @return the post time
    367          */
    368         protected long calculatePostTime() {
    369             return mClock.currentTimeMillis();
    370         }
    371 
    372         /**
    373          * Calculate when the notification should auto-dismiss itself.
    374          * @return the finish time
    375          */
    376         protected long calculateFinishTime() {
    377             return mPostTime + mAutoDismissNotificationDecay;
    378         }
    379     }
    380 
    381     protected final static class Clock {
    382         public long currentTimeMillis() {
    383             return SystemClock.elapsedRealtime();
    384         }
    385     }
    386 }
    387