Home | History | Annotate | Download | only in phone
      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.phone;
     18 
     19 import android.annotation.NonNull;
     20 import android.annotation.Nullable;
     21 import android.content.Context;
     22 import android.content.res.Configuration;
     23 import android.content.res.Resources;
     24 import android.support.v4.util.ArraySet;
     25 import android.util.Log;
     26 import android.util.Pools;
     27 import android.view.View;
     28 import android.view.ViewTreeObserver;
     29 
     30 import com.android.internal.annotations.VisibleForTesting;
     31 import com.android.systemui.Dumpable;
     32 import com.android.systemui.R;
     33 import com.android.systemui.statusbar.ExpandableNotificationRow;
     34 import com.android.systemui.statusbar.NotificationData;
     35 import com.android.systemui.statusbar.StatusBarState;
     36 import com.android.systemui.statusbar.notification.VisualStabilityManager;
     37 import com.android.systemui.statusbar.policy.ConfigurationController;
     38 import com.android.systemui.statusbar.policy.HeadsUpManager;
     39 import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;
     40 
     41 import java.io.FileDescriptor;
     42 import java.io.PrintWriter;
     43 import java.util.HashSet;
     44 import java.util.Stack;
     45 
     46 /**
     47  * A implementation of HeadsUpManager for phone and car.
     48  */
     49 public class HeadsUpManagerPhone extends HeadsUpManager implements Dumpable,
     50        ViewTreeObserver.OnComputeInternalInsetsListener, VisualStabilityManager.Callback,
     51        OnHeadsUpChangedListener, ConfigurationController.ConfigurationListener {
     52     private static final String TAG = "HeadsUpManagerPhone";
     53     private static final boolean DEBUG = false;
     54 
     55     private final View mStatusBarWindowView;
     56     private final NotificationGroupManager mGroupManager;
     57     private final StatusBar mBar;
     58     private final VisualStabilityManager mVisualStabilityManager;
     59     private boolean mReleaseOnExpandFinish;
     60 
     61     private int mStatusBarHeight;
     62     private int mHeadsUpInset;
     63     private boolean mTrackingHeadsUp;
     64     private HashSet<String> mSwipedOutKeys = new HashSet<>();
     65     private HashSet<NotificationData.Entry> mEntriesToRemoveAfterExpand = new HashSet<>();
     66     private ArraySet<NotificationData.Entry> mEntriesToRemoveWhenReorderingAllowed
     67             = new ArraySet<>();
     68     private boolean mIsExpanded;
     69     private int[] mTmpTwoArray = new int[2];
     70     private boolean mHeadsUpGoingAway;
     71     private boolean mWaitingOnCollapseWhenGoingAway;
     72     private boolean mIsObserving;
     73     private int mStatusBarState;
     74 
     75     private final Pools.Pool<HeadsUpEntryPhone> mEntryPool = new Pools.Pool<HeadsUpEntryPhone>() {
     76         private Stack<HeadsUpEntryPhone> mPoolObjects = new Stack<>();
     77 
     78         @Override
     79         public HeadsUpEntryPhone acquire() {
     80             if (!mPoolObjects.isEmpty()) {
     81                 return mPoolObjects.pop();
     82             }
     83             return new HeadsUpEntryPhone();
     84         }
     85 
     86         @Override
     87         public boolean release(@NonNull HeadsUpEntryPhone instance) {
     88             mPoolObjects.push(instance);
     89             return true;
     90         }
     91     };
     92 
     93     ///////////////////////////////////////////////////////////////////////////////////////////////
     94     //  Constructor:
     95 
     96     public HeadsUpManagerPhone(@NonNull final Context context, @NonNull View statusBarWindowView,
     97             @NonNull NotificationGroupManager groupManager, @NonNull StatusBar bar,
     98             @NonNull VisualStabilityManager visualStabilityManager) {
     99         super(context);
    100 
    101         mStatusBarWindowView = statusBarWindowView;
    102         mGroupManager = groupManager;
    103         mBar = bar;
    104         mVisualStabilityManager = visualStabilityManager;
    105 
    106         initResources();
    107 
    108         addListener(new OnHeadsUpChangedListener() {
    109             @Override
    110             public void onHeadsUpPinnedModeChanged(boolean hasPinnedNotification) {
    111                 if (DEBUG) Log.w(TAG, "onHeadsUpPinnedModeChanged");
    112                 updateTouchableRegionListener();
    113             }
    114         });
    115     }
    116 
    117     private void initResources() {
    118         Resources resources = mContext.getResources();
    119         mStatusBarHeight = resources.getDimensionPixelSize(
    120                 com.android.internal.R.dimen.status_bar_height);
    121         mHeadsUpInset = mStatusBarHeight + resources.getDimensionPixelSize(
    122                 R.dimen.heads_up_status_bar_padding);
    123     }
    124 
    125     @Override
    126     public void onDensityOrFontScaleChanged() {
    127         super.onDensityOrFontScaleChanged();
    128         initResources();
    129     }
    130 
    131     ///////////////////////////////////////////////////////////////////////////////////////////////
    132     //  Public methods:
    133 
    134     /**
    135      * Decides whether a click is invalid for a notification, i.e it has not been shown long enough
    136      * that a user might have consciously clicked on it.
    137      *
    138      * @param key the key of the touched notification
    139      * @return whether the touch is invalid and should be discarded
    140      */
    141     public boolean shouldSwallowClick(@NonNull String key) {
    142         HeadsUpManager.HeadsUpEntry entry = getHeadsUpEntry(key);
    143         return entry != null && mClock.currentTimeMillis() < entry.postTime;
    144     }
    145 
    146     public void onExpandingFinished() {
    147         if (mReleaseOnExpandFinish) {
    148             releaseAllImmediately();
    149             mReleaseOnExpandFinish = false;
    150         } else {
    151             for (NotificationData.Entry entry : mEntriesToRemoveAfterExpand) {
    152                 if (isHeadsUp(entry.key)) {
    153                     // Maybe the heads-up was removed already
    154                     removeHeadsUpEntry(entry);
    155                 }
    156             }
    157         }
    158         mEntriesToRemoveAfterExpand.clear();
    159     }
    160 
    161     /**
    162      * Sets the tracking-heads-up flag. If the flag is true, HeadsUpManager doesn't remove the entry
    163      * from the list even after a Heads Up Notification is gone.
    164      */
    165     public void setTrackingHeadsUp(boolean trackingHeadsUp) {
    166         mTrackingHeadsUp = trackingHeadsUp;
    167     }
    168 
    169     /**
    170      * Notify that the status bar panel gets expanded or collapsed.
    171      *
    172      * @param isExpanded True to notify expanded, false to notify collapsed.
    173      */
    174     public void setIsPanelExpanded(boolean isExpanded) {
    175         if (isExpanded != mIsExpanded) {
    176             mIsExpanded = isExpanded;
    177             if (isExpanded) {
    178                 // make sure our state is sane
    179                 mWaitingOnCollapseWhenGoingAway = false;
    180                 mHeadsUpGoingAway = false;
    181                 updateTouchableRegionListener();
    182             }
    183         }
    184     }
    185 
    186     /**
    187      * Set the current state of the statusbar.
    188      */
    189     public void setStatusBarState(int statusBarState) {
    190         mStatusBarState = statusBarState;
    191     }
    192 
    193     /**
    194      * Set that we are exiting the headsUp pinned mode, but some notifications might still be
    195      * animating out. This is used to keep the touchable regions in a sane state.
    196      */
    197     public void setHeadsUpGoingAway(boolean headsUpGoingAway) {
    198         if (headsUpGoingAway != mHeadsUpGoingAway) {
    199             mHeadsUpGoingAway = headsUpGoingAway;
    200             if (!headsUpGoingAway) {
    201                 waitForStatusBarLayout();
    202             }
    203             updateTouchableRegionListener();
    204         }
    205     }
    206 
    207     /**
    208      * Notifies that a remote input textbox in notification gets active or inactive.
    209      * @param entry The entry of the target notification.
    210      * @param remoteInputActive True to notify active, False to notify inactive.
    211      */
    212     public void setRemoteInputActive(
    213             @NonNull NotificationData.Entry entry, boolean remoteInputActive) {
    214         HeadsUpEntryPhone headsUpEntry = getHeadsUpEntryPhone(entry.key);
    215         if (headsUpEntry != null && headsUpEntry.remoteInputActive != remoteInputActive) {
    216             headsUpEntry.remoteInputActive = remoteInputActive;
    217             if (remoteInputActive) {
    218                 headsUpEntry.removeAutoRemovalCallbacks();
    219             } else {
    220                 headsUpEntry.updateEntry(false /* updatePostTime */);
    221             }
    222         }
    223     }
    224 
    225     @VisibleForTesting
    226     public void removeMinimumDisplayTimeForTesting() {
    227         mMinimumDisplayTime = 0;
    228         mHeadsUpNotificationDecay = 0;
    229         mTouchAcceptanceDelay = 0;
    230     }
    231 
    232     ///////////////////////////////////////////////////////////////////////////////////////////////
    233     //  HeadsUpManager public methods overrides:
    234 
    235     @Override
    236     public boolean isTrackingHeadsUp() {
    237         return mTrackingHeadsUp;
    238     }
    239 
    240     @Override
    241     public void snooze() {
    242         super.snooze();
    243         mReleaseOnExpandFinish = true;
    244     }
    245 
    246     /**
    247      * React to the removal of the notification in the heads up.
    248      *
    249      * @return true if the notification was removed and false if it still needs to be kept around
    250      * for a bit since it wasn't shown long enough
    251      */
    252     @Override
    253     public boolean removeNotification(@NonNull String key, boolean ignoreEarliestRemovalTime) {
    254         if (wasShownLongEnough(key) || ignoreEarliestRemovalTime) {
    255             return super.removeNotification(key, ignoreEarliestRemovalTime);
    256         } else {
    257             HeadsUpEntryPhone entry = getHeadsUpEntryPhone(key);
    258             entry.removeAsSoonAsPossible();
    259             return false;
    260         }
    261     }
    262 
    263     public void addSwipedOutNotification(@NonNull String key) {
    264         mSwipedOutKeys.add(key);
    265     }
    266 
    267     ///////////////////////////////////////////////////////////////////////////////////////////////
    268     //  Dumpable overrides:
    269 
    270     @Override
    271     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
    272         pw.println("HeadsUpManagerPhone state:");
    273         dumpInternal(fd, pw, args);
    274     }
    275 
    276     ///////////////////////////////////////////////////////////////////////////////////////////////
    277     //  ViewTreeObserver.OnComputeInternalInsetsListener overrides:
    278 
    279     /**
    280      * Overridden from TreeObserver.
    281      */
    282     @Override
    283     public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo info) {
    284         if (mIsExpanded || mBar.isBouncerShowing()) {
    285             // The touchable region is always the full area when expanded
    286             return;
    287         }
    288         if (hasPinnedHeadsUp()) {
    289             ExpandableNotificationRow topEntry = getTopEntry().row;
    290             if (topEntry.isChildInGroup()) {
    291                 final ExpandableNotificationRow groupSummary
    292                         = mGroupManager.getGroupSummary(topEntry.getStatusBarNotification());
    293                 if (groupSummary != null) {
    294                     topEntry = groupSummary;
    295                 }
    296             }
    297             topEntry.getLocationOnScreen(mTmpTwoArray);
    298             int minX = mTmpTwoArray[0];
    299             int maxX = mTmpTwoArray[0] + topEntry.getWidth();
    300             int height = topEntry.getIntrinsicHeight();
    301 
    302             info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
    303             info.touchableRegion.set(minX, 0, maxX, mHeadsUpInset + height);
    304         } else if (mHeadsUpGoingAway || mWaitingOnCollapseWhenGoingAway) {
    305             info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
    306             info.touchableRegion.set(0, 0, mStatusBarWindowView.getWidth(), mStatusBarHeight);
    307         }
    308     }
    309 
    310     @Override
    311     public void onConfigChanged(Configuration newConfig) {
    312         Resources resources = mContext.getResources();
    313         mStatusBarHeight = resources.getDimensionPixelSize(
    314                 com.android.internal.R.dimen.status_bar_height);
    315     }
    316 
    317     ///////////////////////////////////////////////////////////////////////////////////////////////
    318     //  VisualStabilityManager.Callback overrides:
    319 
    320     @Override
    321     public void onReorderingAllowed() {
    322         mBar.getNotificationScrollLayout().setHeadsUpGoingAwayAnimationsAllowed(false);
    323         for (NotificationData.Entry entry : mEntriesToRemoveWhenReorderingAllowed) {
    324             if (isHeadsUp(entry.key)) {
    325                 // Maybe the heads-up was removed already
    326                 removeHeadsUpEntry(entry);
    327             }
    328         }
    329         mEntriesToRemoveWhenReorderingAllowed.clear();
    330         mBar.getNotificationScrollLayout().setHeadsUpGoingAwayAnimationsAllowed(true);
    331     }
    332 
    333     ///////////////////////////////////////////////////////////////////////////////////////////////
    334     //  HeadsUpManager utility (protected) methods overrides:
    335 
    336     @Override
    337     protected HeadsUpEntry createHeadsUpEntry() {
    338         return mEntryPool.acquire();
    339     }
    340 
    341     @Override
    342     protected void releaseHeadsUpEntry(HeadsUpEntry entry) {
    343         entry.reset();
    344         mEntryPool.release((HeadsUpEntryPhone) entry);
    345     }
    346 
    347     @Override
    348     protected boolean shouldHeadsUpBecomePinned(NotificationData.Entry entry) {
    349           return mStatusBarState != StatusBarState.KEYGUARD && !mIsExpanded
    350                   || super.shouldHeadsUpBecomePinned(entry);
    351     }
    352 
    353     @Override
    354     protected void dumpInternal(FileDescriptor fd, PrintWriter pw, String[] args) {
    355         super.dumpInternal(fd, pw, args);
    356         pw.print("  mStatusBarState="); pw.println(mStatusBarState);
    357     }
    358 
    359     ///////////////////////////////////////////////////////////////////////////////////////////////
    360     //  Private utility methods:
    361 
    362     @Nullable
    363     private HeadsUpEntryPhone getHeadsUpEntryPhone(@NonNull String key) {
    364         return (HeadsUpEntryPhone) getHeadsUpEntry(key);
    365     }
    366 
    367     @Nullable
    368     private HeadsUpEntryPhone getTopHeadsUpEntryPhone() {
    369         return (HeadsUpEntryPhone) getTopHeadsUpEntry();
    370     }
    371 
    372     private boolean wasShownLongEnough(@NonNull String key) {
    373         if (mSwipedOutKeys.contains(key)) {
    374             // We always instantly dismiss views being manually swiped out.
    375             mSwipedOutKeys.remove(key);
    376             return true;
    377         }
    378 
    379         HeadsUpEntryPhone headsUpEntry = getHeadsUpEntryPhone(key);
    380         HeadsUpEntryPhone topEntry = getTopHeadsUpEntryPhone();
    381         return headsUpEntry != topEntry || headsUpEntry.wasShownLongEnough();
    382     }
    383 
    384     /**
    385      * We need to wait on the whole panel to collapse, before we can remove the touchable region
    386      * listener.
    387      */
    388     private void waitForStatusBarLayout() {
    389         mWaitingOnCollapseWhenGoingAway = true;
    390         mStatusBarWindowView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
    391             @Override
    392             public void onLayoutChange(View v, int left, int top, int right, int bottom,
    393                     int oldLeft,
    394                     int oldTop, int oldRight, int oldBottom) {
    395                 if (mStatusBarWindowView.getHeight() <= mStatusBarHeight) {
    396                     mStatusBarWindowView.removeOnLayoutChangeListener(this);
    397                     mWaitingOnCollapseWhenGoingAway = false;
    398                     updateTouchableRegionListener();
    399                 }
    400             }
    401         });
    402     }
    403 
    404     private void updateTouchableRegionListener() {
    405         boolean shouldObserve = hasPinnedHeadsUp() || mHeadsUpGoingAway
    406                 || mWaitingOnCollapseWhenGoingAway;
    407         if (shouldObserve == mIsObserving) {
    408             return;
    409         }
    410         if (shouldObserve) {
    411             mStatusBarWindowView.getViewTreeObserver().addOnComputeInternalInsetsListener(this);
    412             mStatusBarWindowView.requestLayout();
    413         } else {
    414             mStatusBarWindowView.getViewTreeObserver().removeOnComputeInternalInsetsListener(this);
    415         }
    416         mIsObserving = shouldObserve;
    417     }
    418 
    419     ///////////////////////////////////////////////////////////////////////////////////////////////
    420     //  HeadsUpEntryPhone:
    421 
    422     protected class HeadsUpEntryPhone extends HeadsUpManager.HeadsUpEntry {
    423         public void setEntry(@NonNull final NotificationData.Entry entry) {
    424            Runnable removeHeadsUpRunnable = () -> {
    425                 if (!mVisualStabilityManager.isReorderingAllowed()) {
    426                     mEntriesToRemoveWhenReorderingAllowed.add(entry);
    427                     mVisualStabilityManager.addReorderingAllowedCallback(
    428                             HeadsUpManagerPhone.this);
    429                 } else if (!mTrackingHeadsUp) {
    430                     removeHeadsUpEntry(entry);
    431                 } else {
    432                     mEntriesToRemoveAfterExpand.add(entry);
    433                 }
    434             };
    435 
    436             super.setEntry(entry, removeHeadsUpRunnable);
    437         }
    438 
    439         public boolean wasShownLongEnough() {
    440             return earliestRemovaltime < mClock.currentTimeMillis();
    441         }
    442 
    443         @Override
    444         public void updateEntry(boolean updatePostTime) {
    445             super.updateEntry(updatePostTime);
    446 
    447             if (mEntriesToRemoveAfterExpand.contains(entry)) {
    448                 mEntriesToRemoveAfterExpand.remove(entry);
    449             }
    450             if (mEntriesToRemoveWhenReorderingAllowed.contains(entry)) {
    451                 mEntriesToRemoveWhenReorderingAllowed.remove(entry);
    452             }
    453         }
    454 
    455         @Override
    456         public void expanded(boolean expanded) {
    457             if (this.expanded == expanded) {
    458                 return;
    459             }
    460 
    461             this.expanded = expanded;
    462             if (expanded) {
    463                 removeAutoRemovalCallbacks();
    464             } else {
    465                 updateEntry(false /* updatePostTime */);
    466             }
    467         }
    468     }
    469 }
    470