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.View;
22 import android.view.WindowInsets;
23 
24 import com.android.internal.annotations.VisibleForTesting;
25 import com.android.systemui.Dependency;
26 import com.android.systemui.R;
27 import com.android.systemui.statusbar.CrossFadeHelper;
28 import com.android.systemui.statusbar.ExpandableNotificationRow;
29 import com.android.systemui.statusbar.HeadsUpStatusBarView;
30 import com.android.systemui.statusbar.NotificationData;
31 import com.android.systemui.statusbar.policy.DarkIconDispatcher;
32 import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;
33 import com.android.systemui.statusbar.stack.NotificationStackScrollLayout;
34 
35 import java.util.function.BiConsumer;
36 import java.util.function.Consumer;
37 
38 /**
39  * Controls the appearance of heads up notifications in the icon area and the header itself.
40  */
41 public class HeadsUpAppearanceController implements OnHeadsUpChangedListener,
42         DarkIconDispatcher.DarkReceiver {
43     public static final int CONTENT_FADE_DURATION = 110;
44     public static final int CONTENT_FADE_DELAY = 100;
45     private final NotificationIconAreaController mNotificationIconAreaController;
46     private final HeadsUpManagerPhone mHeadsUpManager;
47     private final NotificationStackScrollLayout mStackScroller;
48     private final HeadsUpStatusBarView mHeadsUpStatusBarView;
49     private final View mClockView;
50     private final DarkIconDispatcher mDarkIconDispatcher;
51     private final NotificationPanelView mPanelView;
52     private final Consumer<ExpandableNotificationRow>
53             mSetTrackingHeadsUp = this::setTrackingHeadsUp;
54     private final Runnable mUpdatePanelTranslation = this::updatePanelTranslation;
55     private final BiConsumer<Float, Float> mSetExpandedHeight = this::setExpandedHeight;
56     private float mExpandedHeight;
57     private boolean mIsExpanded;
58     private float mExpandFraction;
59     private ExpandableNotificationRow mTrackedChild;
60     private boolean mShown;
61     private final View.OnLayoutChangeListener mStackScrollLayoutChangeListener =
62             (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom)
63                     -> updatePanelTranslation();
64     Point mPoint;
65 
HeadsUpAppearanceController( NotificationIconAreaController notificationIconAreaController, HeadsUpManagerPhone headsUpManager, View statusbarView)66     public HeadsUpAppearanceController(
67             NotificationIconAreaController notificationIconAreaController,
68             HeadsUpManagerPhone headsUpManager,
69             View statusbarView) {
70         this(notificationIconAreaController, headsUpManager,
71                 statusbarView.findViewById(R.id.heads_up_status_bar_view),
72                 statusbarView.findViewById(R.id.notification_stack_scroller),
73                 statusbarView.findViewById(R.id.notification_panel),
74                 statusbarView.findViewById(R.id.clock));
75     }
76 
77     @VisibleForTesting
HeadsUpAppearanceController( NotificationIconAreaController notificationIconAreaController, HeadsUpManagerPhone headsUpManager, HeadsUpStatusBarView headsUpStatusBarView, NotificationStackScrollLayout stackScroller, NotificationPanelView panelView, View clockView)78     public HeadsUpAppearanceController(
79             NotificationIconAreaController notificationIconAreaController,
80             HeadsUpManagerPhone headsUpManager,
81             HeadsUpStatusBarView headsUpStatusBarView,
82             NotificationStackScrollLayout stackScroller,
83             NotificationPanelView panelView,
84             View clockView) {
85         mNotificationIconAreaController = notificationIconAreaController;
86         mHeadsUpManager = headsUpManager;
87         mHeadsUpManager.addListener(this);
88         mHeadsUpStatusBarView = headsUpStatusBarView;
89         headsUpStatusBarView.setOnDrawingRectChangedListener(
90                 () -> updateIsolatedIconLocation(true /* requireUpdate */));
91         mStackScroller = stackScroller;
92         mPanelView = panelView;
93         panelView.addTrackingHeadsUpListener(mSetTrackingHeadsUp);
94         panelView.addVerticalTranslationListener(mUpdatePanelTranslation);
95         panelView.setHeadsUpAppearanceController(this);
96         mStackScroller.addOnExpandedHeightListener(mSetExpandedHeight);
97         mStackScroller.addOnLayoutChangeListener(mStackScrollLayoutChangeListener);
98         mStackScroller.setHeadsUpAppearanceController(this);
99         mClockView = clockView;
100         mDarkIconDispatcher = Dependency.get(DarkIconDispatcher.class);
101         mDarkIconDispatcher.addDarkReceiver(this);
102     }
103 
104 
destroy()105     public void destroy() {
106         mHeadsUpManager.removeListener(this);
107         mHeadsUpStatusBarView.setOnDrawingRectChangedListener(null);
108         mPanelView.removeTrackingHeadsUpListener(mSetTrackingHeadsUp);
109         mPanelView.removeVerticalTranslationListener(mUpdatePanelTranslation);
110         mPanelView.setHeadsUpAppearanceController(null);
111         mStackScroller.removeOnExpandedHeightListener(mSetExpandedHeight);
112         mStackScroller.removeOnLayoutChangeListener(mStackScrollLayoutChangeListener);
113         mDarkIconDispatcher.removeDarkReceiver(this);
114     }
115 
updateIsolatedIconLocation(boolean requireStateUpdate)116     private void updateIsolatedIconLocation(boolean requireStateUpdate) {
117         mNotificationIconAreaController.setIsolatedIconLocation(
118                 mHeadsUpStatusBarView.getIconDrawingRect(), requireStateUpdate);
119     }
120 
121     @Override
onHeadsUpPinned(ExpandableNotificationRow headsUp)122     public void onHeadsUpPinned(ExpandableNotificationRow headsUp) {
123         updateTopEntry();
124         updateHeader(headsUp.getEntry());
125     }
126 
127     /** To count the distance from the window right boundary to scroller right boundary. The
128      * distance formula is the following:
129      *     Y = screenSize - (SystemWindow's width + Scroller.getRight())
130      * There are four modes MUST to be considered in Cut Out of RTL.
131      * No Cut Out:
132      *   Scroller + NB
133      *   NB + Scroller
134      *     => SystemWindow = NavigationBar's width
135      *     => Y = screenSize - (SystemWindow's width + Scroller.getRight())
136      * Corner Cut Out or Tall Cut Out:
137      *   cut out + Scroller + NB
138      *   NB + Scroller + cut out
139      *     => SystemWindow = NavigationBar's width
140      *     => Y = screenSize - (SystemWindow's width + Scroller.getRight())
141      * Double Cut Out:
142      *   cut out left + Scroller + (NB + cut out right)
143      *     SystemWindow = NavigationBar's width + cut out right width
144      *     => Y = screenSize - (SystemWindow's width + Scroller.getRight())
145      *   (cut out left + NB) + Scroller + cut out right
146      *     SystemWindow = NavigationBar's width + cut out left width
147      *     => Y = screenSize - (SystemWindow's width + Scroller.getRight())
148      * @return the translation X value for RTL. In theory, it should be negative. i.e. -Y
149      */
getRtlTranslation()150     private int getRtlTranslation() {
151         if (mPoint == null) {
152             mPoint = new Point();
153         }
154 
155         int realDisplaySize = 0;
156         if (mStackScroller.getDisplay() != null) {
157             mStackScroller.getDisplay().getRealSize(mPoint);
158             realDisplaySize = mPoint.x;
159         }
160 
161         WindowInsets windowInset = mStackScroller.getRootWindowInsets();
162         return windowInset.getSystemWindowInsetLeft() + mStackScroller.getRight()
163                 + windowInset.getSystemWindowInsetRight() - realDisplaySize;
164     }
165 
updatePanelTranslation()166     public void updatePanelTranslation() {
167         float newTranslation;
168         if (mStackScroller.isLayoutRtl()) {
169             newTranslation = getRtlTranslation();
170         } else {
171             newTranslation = mStackScroller.getLeft();
172         }
173         newTranslation += mStackScroller.getTranslationX();
174         mHeadsUpStatusBarView.setPanelTranslation(newTranslation);
175     }
176 
updateTopEntry()177     private void updateTopEntry() {
178         NotificationData.Entry newEntry = null;
179         if (!mIsExpanded && mHeadsUpManager.hasPinnedHeadsUp()) {
180             newEntry = mHeadsUpManager.getTopEntry();
181         }
182         NotificationData.Entry previousEntry = mHeadsUpStatusBarView.getShowingEntry();
183         mHeadsUpStatusBarView.setEntry(newEntry);
184         if (newEntry != previousEntry) {
185             boolean animateIsolation = false;
186             if (newEntry == null) {
187                 // no heads up anymore, lets start the disappear animation
188 
189                 setShown(false);
190                 animateIsolation = !mIsExpanded;
191             } else if (previousEntry == null) {
192                 // We now have a headsUp and didn't have one before. Let's start the disappear
193                 // animation
194                 setShown(true);
195                 animateIsolation = !mIsExpanded;
196             }
197             updateIsolatedIconLocation(false /* requireUpdate */);
198             mNotificationIconAreaController.showIconIsolated(newEntry == null ? null
199                     : newEntry.icon, animateIsolation);
200         }
201     }
202 
setShown(boolean isShown)203     private void setShown(boolean isShown) {
204         if (mShown != isShown) {
205             mShown = isShown;
206             if (isShown) {
207                 mHeadsUpStatusBarView.setVisibility(View.VISIBLE);
208                 CrossFadeHelper.fadeIn(mHeadsUpStatusBarView, CONTENT_FADE_DURATION /* duration */,
209                         CONTENT_FADE_DELAY /* delay */);
210                 CrossFadeHelper.fadeOut(mClockView, CONTENT_FADE_DURATION/* duration */,
211                         0 /* delay */, () -> mClockView.setVisibility(View.INVISIBLE));
212             } else {
213                 CrossFadeHelper.fadeIn(mClockView, CONTENT_FADE_DURATION /* duration */,
214                         CONTENT_FADE_DELAY /* delay */);
215                 CrossFadeHelper.fadeOut(mHeadsUpStatusBarView, CONTENT_FADE_DURATION/* duration */,
216                         0 /* delay */, () -> mHeadsUpStatusBarView.setVisibility(View.GONE));
217 
218             }
219         }
220     }
221 
222     @VisibleForTesting
isShown()223     public boolean isShown() {
224         return mShown;
225     }
226 
227     /**
228      * Should the headsup status bar view be visible right now? This may be different from isShown,
229      * since the headsUp manager might not have notified us yet of the state change.
230      *
231      * @return if the heads up status bar view should be shown
232      */
shouldBeVisible()233     public boolean shouldBeVisible() {
234         return !mIsExpanded && mHeadsUpManager.hasPinnedHeadsUp();
235     }
236 
237     @Override
onHeadsUpUnPinned(ExpandableNotificationRow headsUp)238     public void onHeadsUpUnPinned(ExpandableNotificationRow headsUp) {
239         updateTopEntry();
240         updateHeader(headsUp.getEntry());
241     }
242 
setExpandedHeight(float expandedHeight, float appearFraction)243     public void setExpandedHeight(float expandedHeight, float appearFraction) {
244         boolean changedHeight = expandedHeight != mExpandedHeight;
245         mExpandedHeight = expandedHeight;
246         mExpandFraction = appearFraction;
247         boolean isExpanded = expandedHeight > 0;
248         if (changedHeight) {
249             updateHeadsUpHeaders();
250         }
251         if (isExpanded != mIsExpanded) {
252             mIsExpanded = isExpanded;
253             updateTopEntry();
254         }
255     }
256 
257     /**
258      * Set a headsUp to be tracked, meaning that it is currently being pulled down after being
259      * in a pinned state on the top. The expand animation is different in that case and we need
260      * to update the header constantly afterwards.
261      *
262      * @param trackedChild the tracked headsUp or null if it's not tracking anymore.
263      */
setTrackingHeadsUp(ExpandableNotificationRow trackedChild)264     public void setTrackingHeadsUp(ExpandableNotificationRow trackedChild) {
265         ExpandableNotificationRow previousTracked = mTrackedChild;
266         mTrackedChild = trackedChild;
267         if (previousTracked != null) {
268             updateHeader(previousTracked.getEntry());
269         }
270     }
271 
updateHeadsUpHeaders()272     private void updateHeadsUpHeaders() {
273         mHeadsUpManager.getAllEntries().forEach(entry -> {
274             updateHeader(entry);
275         });
276     }
277 
updateHeader(NotificationData.Entry entry)278     public void updateHeader(NotificationData.Entry entry) {
279         ExpandableNotificationRow row = entry.row;
280         float headerVisibleAmount = 1.0f;
281         if (row.isPinned() || row.isHeadsUpAnimatingAway() || row == mTrackedChild) {
282             headerVisibleAmount = mExpandFraction;
283         }
284         row.setHeaderVisibleAmount(headerVisibleAmount);
285     }
286 
287     @Override
onDarkChanged(Rect area, float darkIntensity, int tint)288     public void onDarkChanged(Rect area, float darkIntensity, int tint) {
289         mHeadsUpStatusBarView.onDarkChanged(area, darkIntensity, tint);
290     }
291 
setPublicMode(boolean publicMode)292     public void setPublicMode(boolean publicMode) {
293         mHeadsUpStatusBarView.setPublicMode(publicMode);
294         updateTopEntry();
295     }
296 }
297