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