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 static com.android.systemui.SysUiServiceProvider.getComponent;
20 
21 import android.graphics.Point;
22 import android.graphics.Rect;
23 import android.view.DisplayCutout;
24 import android.view.View;
25 import android.view.WindowInsets;
26 
27 import com.android.internal.annotations.VisibleForTesting;
28 import com.android.internal.widget.ViewClippingUtil;
29 import com.android.systemui.Dependency;
30 import com.android.systemui.R;
31 import com.android.systemui.plugins.DarkIconDispatcher;
32 import com.android.systemui.plugins.statusbar.StatusBarStateController;
33 import com.android.systemui.statusbar.CommandQueue;
34 import com.android.systemui.statusbar.CrossFadeHelper;
35 import com.android.systemui.statusbar.HeadsUpStatusBarView;
36 import com.android.systemui.statusbar.StatusBarState;
37 import com.android.systemui.statusbar.SysuiStatusBarStateController;
38 import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator;
39 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
40 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
41 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout;
42 import com.android.systemui.statusbar.policy.KeyguardMonitor;
43 import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;
44 
45 import java.util.function.BiConsumer;
46 import java.util.function.Consumer;
47 
48 /**
49  * Controls the appearance of heads up notifications in the icon area and the header itself.
50  */
51 public class HeadsUpAppearanceController implements OnHeadsUpChangedListener,
52         DarkIconDispatcher.DarkReceiver, NotificationWakeUpCoordinator.WakeUpListener {
53     public static final int CONTENT_FADE_DURATION = 110;
54     public static final int CONTENT_FADE_DELAY = 100;
55     private final NotificationIconAreaController mNotificationIconAreaController;
56     private final HeadsUpManagerPhone mHeadsUpManager;
57     private final NotificationStackScrollLayout mStackScroller;
58     private final HeadsUpStatusBarView mHeadsUpStatusBarView;
59     private final View mCenteredIconView;
60     private final View mClockView;
61     private final View mOperatorNameView;
62     private final DarkIconDispatcher mDarkIconDispatcher;
63     private final NotificationPanelView mPanelView;
64     private final Consumer<ExpandableNotificationRow>
65             mSetTrackingHeadsUp = this::setTrackingHeadsUp;
66     private final Runnable mUpdatePanelTranslation = this::updatePanelTranslation;
67     private final BiConsumer<Float, Float> mSetExpandedHeight = this::setAppearFraction;
68     private final KeyguardBypassController mBypassController;
69     private final StatusBarStateController mStatusBarStateController;
70     private final CommandQueue mCommandQueue;
71     private final NotificationWakeUpCoordinator mWakeUpCoordinator;
72     @VisibleForTesting
73     float mExpandedHeight;
74     @VisibleForTesting
75     boolean mIsExpanded;
76     @VisibleForTesting
77     float mAppearFraction;
78     private ExpandableNotificationRow mTrackedChild;
79     private boolean mShown;
80     private final View.OnLayoutChangeListener mStackScrollLayoutChangeListener =
81             (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom)
82                     -> updatePanelTranslation();
83     private final ViewClippingUtil.ClippingParameters mParentClippingParams =
84             new ViewClippingUtil.ClippingParameters() {
85                 @Override
86                 public boolean shouldFinish(View view) {
87                     return view.getId() == R.id.status_bar;
88                 }
89             };
90     private boolean mAnimationsEnabled = true;
91     Point mPoint;
92     private KeyguardMonitor mKeyguardMonitor;
93 
94 
HeadsUpAppearanceController( NotificationIconAreaController notificationIconAreaController, HeadsUpManagerPhone headsUpManager, View statusbarView, SysuiStatusBarStateController statusBarStateController, KeyguardBypassController keyguardBypassController, NotificationWakeUpCoordinator wakeUpCoordinator)95     public HeadsUpAppearanceController(
96             NotificationIconAreaController notificationIconAreaController,
97             HeadsUpManagerPhone headsUpManager,
98             View statusbarView,
99             SysuiStatusBarStateController statusBarStateController,
100             KeyguardBypassController keyguardBypassController,
101             NotificationWakeUpCoordinator wakeUpCoordinator) {
102         this(notificationIconAreaController, headsUpManager, statusBarStateController,
103                 keyguardBypassController, wakeUpCoordinator,
104                 statusbarView.findViewById(R.id.heads_up_status_bar_view),
105                 statusbarView.findViewById(R.id.notification_stack_scroller),
106                 statusbarView.findViewById(R.id.notification_panel),
107                 statusbarView.findViewById(R.id.clock),
108                 statusbarView.findViewById(R.id.operator_name_frame),
109                 statusbarView.findViewById(R.id.centered_icon_area));
110     }
111 
112     @VisibleForTesting
HeadsUpAppearanceController( NotificationIconAreaController notificationIconAreaController, HeadsUpManagerPhone headsUpManager, StatusBarStateController stateController, KeyguardBypassController bypassController, NotificationWakeUpCoordinator wakeUpCoordinator, HeadsUpStatusBarView headsUpStatusBarView, NotificationStackScrollLayout stackScroller, NotificationPanelView panelView, View clockView, View operatorNameView, View centeredIconView)113     public HeadsUpAppearanceController(
114             NotificationIconAreaController notificationIconAreaController,
115             HeadsUpManagerPhone headsUpManager,
116             StatusBarStateController stateController,
117             KeyguardBypassController bypassController,
118             NotificationWakeUpCoordinator wakeUpCoordinator,
119             HeadsUpStatusBarView headsUpStatusBarView,
120             NotificationStackScrollLayout stackScroller,
121             NotificationPanelView panelView,
122             View clockView,
123             View operatorNameView,
124             View centeredIconView) {
125         mNotificationIconAreaController = notificationIconAreaController;
126         mHeadsUpManager = headsUpManager;
127         mHeadsUpManager.addListener(this);
128         mHeadsUpStatusBarView = headsUpStatusBarView;
129         mCenteredIconView = centeredIconView;
130         headsUpStatusBarView.setOnDrawingRectChangedListener(
131                 () -> updateIsolatedIconLocation(true /* requireUpdate */));
132         mStackScroller = stackScroller;
133         mPanelView = panelView;
134         panelView.addTrackingHeadsUpListener(mSetTrackingHeadsUp);
135         panelView.addVerticalTranslationListener(mUpdatePanelTranslation);
136         panelView.setHeadsUpAppearanceController(this);
137         mStackScroller.addOnExpandedHeightChangedListener(mSetExpandedHeight);
138         mStackScroller.addOnLayoutChangeListener(mStackScrollLayoutChangeListener);
139         mStackScroller.setHeadsUpAppearanceController(this);
140         mClockView = clockView;
141         mOperatorNameView = operatorNameView;
142         mDarkIconDispatcher = Dependency.get(DarkIconDispatcher.class);
143         mDarkIconDispatcher.addDarkReceiver(this);
144 
145         mHeadsUpStatusBarView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
146             @Override
147             public void onLayoutChange(View v, int left, int top, int right, int bottom,
148                     int oldLeft, int oldTop, int oldRight, int oldBottom) {
149                 if (shouldBeVisible()) {
150                     updateTopEntry();
151 
152                     // trigger scroller to notify the latest panel translation
153                     mStackScroller.requestLayout();
154                 }
155                 mHeadsUpStatusBarView.removeOnLayoutChangeListener(this);
156             }
157         });
158         mBypassController = bypassController;
159         mStatusBarStateController = stateController;
160         mWakeUpCoordinator = wakeUpCoordinator;
161         wakeUpCoordinator.addListener(this);
162         mCommandQueue = getComponent(headsUpStatusBarView.getContext(), CommandQueue.class);
163         mKeyguardMonitor = Dependency.get(KeyguardMonitor.class);
164     }
165 
166 
destroy()167     public void destroy() {
168         mHeadsUpManager.removeListener(this);
169         mHeadsUpStatusBarView.setOnDrawingRectChangedListener(null);
170         mWakeUpCoordinator.removeListener(this);
171         mPanelView.removeTrackingHeadsUpListener(mSetTrackingHeadsUp);
172         mPanelView.removeVerticalTranslationListener(mUpdatePanelTranslation);
173         mPanelView.setHeadsUpAppearanceController(null);
174         mStackScroller.removeOnExpandedHeightChangedListener(mSetExpandedHeight);
175         mStackScroller.removeOnLayoutChangeListener(mStackScrollLayoutChangeListener);
176         mDarkIconDispatcher.removeDarkReceiver(this);
177     }
178 
updateIsolatedIconLocation(boolean requireStateUpdate)179     private void updateIsolatedIconLocation(boolean requireStateUpdate) {
180         mNotificationIconAreaController.setIsolatedIconLocation(
181                 mHeadsUpStatusBarView.getIconDrawingRect(), requireStateUpdate);
182     }
183 
184     @Override
onHeadsUpPinned(NotificationEntry entry)185     public void onHeadsUpPinned(NotificationEntry entry) {
186         updateTopEntry();
187         updateHeader(entry);
188     }
189 
190     /** To count the distance from the window right boundary to scroller right boundary. The
191      * distance formula is the following:
192      *     Y = screenSize - (SystemWindow's width + Scroller.getRight())
193      * There are four modes MUST to be considered in Cut Out of RTL.
194      * No Cut Out:
195      *   Scroller + NB
196      *   NB + Scroller
197      *     => SystemWindow = NavigationBar's width
198      *     => Y = screenSize - (SystemWindow's width + Scroller.getRight())
199      * Corner Cut Out or Tall Cut Out:
200      *   cut out + Scroller + NB
201      *   NB + Scroller + cut out
202      *     => SystemWindow = NavigationBar's width
203      *     => Y = screenSize - (SystemWindow's width + Scroller.getRight())
204      * Double Cut Out:
205      *   cut out left + Scroller + (NB + cut out right)
206      *     SystemWindow = NavigationBar's width + cut out right width
207      *     => Y = screenSize - (SystemWindow's width + Scroller.getRight())
208      *   (cut out left + NB) + Scroller + cut out right
209      *     SystemWindow = NavigationBar's width + cut out left width
210      *     => Y = screenSize - (SystemWindow's width + Scroller.getRight())
211      * @return the translation X value for RTL. In theory, it should be negative. i.e. -Y
212      */
getRtlTranslation()213     private int getRtlTranslation() {
214         if (mPoint == null) {
215             mPoint = new Point();
216         }
217 
218         int realDisplaySize = 0;
219         if (mStackScroller.getDisplay() != null) {
220             mStackScroller.getDisplay().getRealSize(mPoint);
221             realDisplaySize = mPoint.x;
222         }
223 
224         WindowInsets windowInset = mStackScroller.getRootWindowInsets();
225         DisplayCutout cutout = (windowInset != null) ? windowInset.getDisplayCutout() : null;
226         int sysWinLeft = (windowInset != null) ? windowInset.getStableInsetLeft() : 0;
227         int sysWinRight = (windowInset != null) ? windowInset.getStableInsetRight() : 0;
228         int cutoutLeft = (cutout != null) ? cutout.getSafeInsetLeft() : 0;
229         int cutoutRight = (cutout != null) ? cutout.getSafeInsetRight() : 0;
230         int leftInset = Math.max(sysWinLeft, cutoutLeft);
231         int rightInset = Math.max(sysWinRight, cutoutRight);
232 
233         return leftInset + mStackScroller.getRight() + rightInset - realDisplaySize;
234     }
235 
updatePanelTranslation()236     public void updatePanelTranslation() {
237         float newTranslation;
238         if (mStackScroller.isLayoutRtl()) {
239             newTranslation = getRtlTranslation();
240         } else {
241             newTranslation = mStackScroller.getLeft();
242         }
243         newTranslation += mStackScroller.getTranslationX();
244         mHeadsUpStatusBarView.setPanelTranslation(newTranslation);
245     }
246 
updateTopEntry()247     private void updateTopEntry() {
248         NotificationEntry newEntry = null;
249         if (shouldBeVisible()) {
250             newEntry = mHeadsUpManager.getTopEntry();
251         }
252         NotificationEntry previousEntry = mHeadsUpStatusBarView.getShowingEntry();
253         mHeadsUpStatusBarView.setEntry(newEntry);
254         if (newEntry != previousEntry) {
255             boolean animateIsolation = false;
256             if (newEntry == null) {
257                 // no heads up anymore, lets start the disappear animation
258 
259                 setShown(false);
260                 animateIsolation = !mIsExpanded;
261             } else if (previousEntry == null) {
262                 // We now have a headsUp and didn't have one before. Let's start the disappear
263                 // animation
264                 setShown(true);
265                 animateIsolation = !mIsExpanded;
266             }
267             updateIsolatedIconLocation(false /* requireUpdate */);
268             mNotificationIconAreaController.showIconIsolated(newEntry == null ? null
269                     : newEntry.icon, animateIsolation);
270         }
271     }
272 
setShown(boolean isShown)273     private void setShown(boolean isShown) {
274         if (mShown != isShown) {
275             mShown = isShown;
276             if (isShown) {
277                 updateParentClipping(false /* shouldClip */);
278                 mHeadsUpStatusBarView.setVisibility(View.VISIBLE);
279                 show(mHeadsUpStatusBarView);
280                 hide(mClockView, View.INVISIBLE);
281                 if (mCenteredIconView.getVisibility() != View.GONE) {
282                     hide(mCenteredIconView, View.INVISIBLE);
283                 }
284                 if (mOperatorNameView != null) {
285                     hide(mOperatorNameView, View.INVISIBLE);
286                 }
287             } else {
288                 show(mClockView);
289                 if (mCenteredIconView.getVisibility() != View.GONE) {
290                     show(mCenteredIconView);
291                 }
292                 if (mOperatorNameView != null) {
293                     show(mOperatorNameView);
294                 }
295                 hide(mHeadsUpStatusBarView, View.GONE, () -> {
296                     updateParentClipping(true /* shouldClip */);
297                 });
298             }
299             // Show the status bar icons when the view gets shown / hidden
300             if (mStatusBarStateController.getState() != StatusBarState.SHADE) {
301                 mCommandQueue.recomputeDisableFlags(
302                         mHeadsUpStatusBarView.getContext().getDisplayId(), false);
303             }
304         }
305     }
306 
updateParentClipping(boolean shouldClip)307     private void updateParentClipping(boolean shouldClip) {
308         ViewClippingUtil.setClippingDeactivated(
309                 mHeadsUpStatusBarView, !shouldClip, mParentClippingParams);
310     }
311 
312     /**
313      * Hides the view and sets the state to endState when finished.
314      *
315      * @param view The view to hide.
316      * @param endState One of {@link View#INVISIBLE} or {@link View#GONE}.
317      * @see HeadsUpAppearanceController#hide(View, int, Runnable)
318      * @see View#setVisibility(int)
319      *
320      */
hide(View view, int endState)321     private void hide(View view, int endState) {
322         hide(view, endState, null);
323     }
324 
325     /**
326      * Hides the view and sets the state to endState when finished.
327      *
328      * @param view The view to hide.
329      * @param endState One of {@link View#INVISIBLE} or {@link View#GONE}.
330      * @param callback Runnable to be executed after the view has been hidden.
331      * @see View#setVisibility(int)
332      *
333      */
hide(View view, int endState, Runnable callback)334     private void hide(View view, int endState, Runnable callback) {
335         if (mAnimationsEnabled) {
336             CrossFadeHelper.fadeOut(view, CONTENT_FADE_DURATION /* duration */,
337                     0 /* delay */, () -> {
338                         view.setVisibility(endState);
339                         if (callback != null) {
340                             callback.run();
341                         }
342                     });
343         } else {
344             view.setVisibility(endState);
345             if (callback != null) {
346                 callback.run();
347             }
348         }
349     }
350 
show(View view)351     private void show(View view) {
352         if (mAnimationsEnabled) {
353             CrossFadeHelper.fadeIn(view, CONTENT_FADE_DURATION /* duration */,
354                     CONTENT_FADE_DELAY /* delay */);
355         } else {
356             view.setVisibility(View.VISIBLE);
357         }
358     }
359 
360     @VisibleForTesting
setAnimationsEnabled(boolean enabled)361     void setAnimationsEnabled(boolean enabled) {
362         mAnimationsEnabled = enabled;
363     }
364 
365     @VisibleForTesting
isShown()366     public boolean isShown() {
367         return mShown;
368     }
369 
370     /**
371      * Should the headsup status bar view be visible right now? This may be different from isShown,
372      * since the headsUp manager might not have notified us yet of the state change.
373      *
374      * @return if the heads up status bar view should be shown
375      */
shouldBeVisible()376     public boolean shouldBeVisible() {
377         boolean notificationsShown = !mWakeUpCoordinator.getNotificationsFullyHidden();
378         boolean canShow = !mIsExpanded && notificationsShown;
379         if (mBypassController.getBypassEnabled() &&
380                 (mStatusBarStateController.getState() == StatusBarState.KEYGUARD
381                         || mKeyguardMonitor.isKeyguardGoingAway())
382                 && notificationsShown) {
383             canShow = true;
384         }
385         return canShow && mHeadsUpManager.hasPinnedHeadsUp();
386     }
387 
388     @Override
onHeadsUpUnPinned(NotificationEntry entry)389     public void onHeadsUpUnPinned(NotificationEntry entry) {
390         updateTopEntry();
391         updateHeader(entry);
392     }
393 
setAppearFraction(float expandedHeight, float appearFraction)394     public void setAppearFraction(float expandedHeight, float appearFraction) {
395         boolean changed = expandedHeight != mExpandedHeight;
396         mExpandedHeight = expandedHeight;
397         mAppearFraction = appearFraction;
398         boolean isExpanded = expandedHeight > 0;
399         // We only notify if the expandedHeight changed and not on the appearFraction, since
400         // otherwise we may run into an infinite loop where the panel and this are constantly
401         // updating themselves over just a small fraction
402         if (changed) {
403             updateHeadsUpHeaders();
404         }
405         if (isExpanded != mIsExpanded) {
406             mIsExpanded = isExpanded;
407             updateTopEntry();
408         }
409     }
410 
411     /**
412      * Set a headsUp to be tracked, meaning that it is currently being pulled down after being
413      * in a pinned state on the top. The expand animation is different in that case and we need
414      * to update the header constantly afterwards.
415      *
416      * @param trackedChild the tracked headsUp or null if it's not tracking anymore.
417      */
setTrackingHeadsUp(ExpandableNotificationRow trackedChild)418     public void setTrackingHeadsUp(ExpandableNotificationRow trackedChild) {
419         ExpandableNotificationRow previousTracked = mTrackedChild;
420         mTrackedChild = trackedChild;
421         if (previousTracked != null) {
422             updateHeader(previousTracked.getEntry());
423         }
424     }
425 
updateHeadsUpHeaders()426     private void updateHeadsUpHeaders() {
427         mHeadsUpManager.getAllEntries().forEach(entry -> {
428             updateHeader(entry);
429         });
430     }
431 
updateHeader(NotificationEntry entry)432     public void updateHeader(NotificationEntry entry) {
433         ExpandableNotificationRow row = entry.getRow();
434         float headerVisibleAmount = 1.0f;
435         if (row.isPinned() || row.isHeadsUpAnimatingAway() || row == mTrackedChild
436                 || row.showingPulsing()) {
437             headerVisibleAmount = mAppearFraction;
438         }
439         row.setHeaderVisibleAmount(headerVisibleAmount);
440     }
441 
442     @Override
onDarkChanged(Rect area, float darkIntensity, int tint)443     public void onDarkChanged(Rect area, float darkIntensity, int tint) {
444         mHeadsUpStatusBarView.onDarkChanged(area, darkIntensity, tint);
445     }
446 
onStateChanged()447     public void onStateChanged() {
448         updateTopEntry();
449     }
450 
readFrom(HeadsUpAppearanceController oldController)451     void readFrom(HeadsUpAppearanceController oldController) {
452         if (oldController != null) {
453             mTrackedChild = oldController.mTrackedChild;
454             mExpandedHeight = oldController.mExpandedHeight;
455             mIsExpanded = oldController.mIsExpanded;
456             mAppearFraction = oldController.mAppearFraction;
457         }
458     }
459 
460     @Override
onFullyHiddenChanged(boolean isFullyHidden)461     public void onFullyHiddenChanged(boolean isFullyHidden) {
462         updateTopEntry();
463     }
464 }
465