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 
23 import com.android.internal.annotations.VisibleForTesting;
24 import com.android.internal.widget.ViewClippingUtil;
25 import com.android.systemui.Dependency;
26 import com.android.systemui.R;
27 import com.android.systemui.dagger.qualifiers.RootView;
28 import com.android.systemui.plugins.DarkIconDispatcher;
29 import com.android.systemui.plugins.statusbar.StatusBarStateController;
30 import com.android.systemui.statusbar.CommandQueue;
31 import com.android.systemui.statusbar.CrossFadeHelper;
32 import com.android.systemui.statusbar.HeadsUpStatusBarView;
33 import com.android.systemui.statusbar.StatusBarState;
34 import com.android.systemui.statusbar.SysuiStatusBarStateController;
35 import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator;
36 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
37 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
38 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController;
39 import com.android.systemui.statusbar.phone.fragment.dagger.StatusBarFragmentScope;
40 import com.android.systemui.statusbar.policy.KeyguardStateController;
41 import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;
42 import com.android.systemui.util.ViewController;
43 
44 import java.util.function.BiConsumer;
45 import java.util.function.Consumer;
46 
47 import javax.inject.Inject;
48 
49 /**
50  * Controls the appearance of heads up notifications in the icon area and the header itself.
51  */
52 @StatusBarFragmentScope
53 public class HeadsUpAppearanceController extends ViewController<HeadsUpStatusBarView>
54         implements OnHeadsUpChangedListener,
55         DarkIconDispatcher.DarkReceiver,
56         NotificationWakeUpCoordinator.WakeUpListener {
57     public static final int CONTENT_FADE_DURATION = 110;
58     public static final int CONTENT_FADE_DELAY = 100;
59     private final NotificationIconAreaController mNotificationIconAreaController;
60     private final HeadsUpManagerPhone mHeadsUpManager;
61     private final NotificationStackScrollLayoutController mStackScrollerController;
62     private final View mCenteredIconView;
63     private final View mClockView;
64     private final View mOperatorNameView;
65     private final DarkIconDispatcher mDarkIconDispatcher;
66     private final NotificationPanelViewController mNotificationPanelViewController;
67     private final Consumer<ExpandableNotificationRow>
68             mSetTrackingHeadsUp = this::setTrackingHeadsUp;
69     private final BiConsumer<Float, Float> mSetExpandedHeight = this::setAppearFraction;
70     private final KeyguardBypassController mBypassController;
71     private final StatusBarStateController mStatusBarStateController;
72     private final CommandQueue mCommandQueue;
73     private final NotificationWakeUpCoordinator mWakeUpCoordinator;
74     @VisibleForTesting
75     float mExpandedHeight;
76     @VisibleForTesting
77     float mAppearFraction;
78     private ExpandableNotificationRow mTrackedChild;
79     private boolean mShown;
80     private final ViewClippingUtil.ClippingParameters mParentClippingParams =
81             new ViewClippingUtil.ClippingParameters() {
82                 @Override
83                 public boolean shouldFinish(View view) {
84                     return view.getId() == R.id.status_bar;
85                 }
86             };
87     private boolean mAnimationsEnabled = true;
88     Point mPoint;
89     private KeyguardStateController mKeyguardStateController;
90 
91     @Inject
HeadsUpAppearanceController( NotificationIconAreaController notificationIconAreaController, HeadsUpManagerPhone headsUpManager, NotificationStackScrollLayoutController notificationStackScrollLayoutController, SysuiStatusBarStateController statusBarStateController, KeyguardBypassController keyguardBypassController, KeyguardStateController keyguardStateController, NotificationWakeUpCoordinator wakeUpCoordinator, CommandQueue commandQueue, NotificationPanelViewController notificationPanelViewController, @RootView PhoneStatusBarView statusBarView)92     public HeadsUpAppearanceController(
93             NotificationIconAreaController notificationIconAreaController,
94             HeadsUpManagerPhone headsUpManager,
95             NotificationStackScrollLayoutController notificationStackScrollLayoutController,
96             SysuiStatusBarStateController statusBarStateController,
97             KeyguardBypassController keyguardBypassController,
98             KeyguardStateController keyguardStateController,
99             NotificationWakeUpCoordinator wakeUpCoordinator, CommandQueue commandQueue,
100             NotificationPanelViewController notificationPanelViewController,
101             @RootView PhoneStatusBarView statusBarView) {
102         this(notificationIconAreaController, headsUpManager, statusBarStateController,
103                 keyguardBypassController, wakeUpCoordinator, keyguardStateController,
104                 commandQueue, notificationStackScrollLayoutController,
105                 notificationPanelViewController,
106                 // TODO(b/205609837): We should have the StatusBarFragmentComponent provide these
107                 //  four views, and then we can delete this constructor and just use the one below
108                 //  (which also removes the undesirable @VisibleForTesting).
109                 statusBarView.findViewById(R.id.heads_up_status_bar_view),
110                 statusBarView.findViewById(R.id.clock),
111                 statusBarView.findViewById(R.id.operator_name_frame),
112                 statusBarView.findViewById(R.id.centered_icon_area));
113     }
114 
115     @VisibleForTesting
HeadsUpAppearanceController( NotificationIconAreaController notificationIconAreaController, HeadsUpManagerPhone headsUpManager, StatusBarStateController stateController, KeyguardBypassController bypassController, NotificationWakeUpCoordinator wakeUpCoordinator, KeyguardStateController keyguardStateController, CommandQueue commandQueue, NotificationStackScrollLayoutController stackScrollerController, NotificationPanelViewController notificationPanelViewController, HeadsUpStatusBarView headsUpStatusBarView, View clockView, View operatorNameView, View centeredIconView)116     public HeadsUpAppearanceController(
117             NotificationIconAreaController notificationIconAreaController,
118             HeadsUpManagerPhone headsUpManager,
119             StatusBarStateController stateController,
120             KeyguardBypassController bypassController,
121             NotificationWakeUpCoordinator wakeUpCoordinator,
122             KeyguardStateController keyguardStateController,
123             CommandQueue commandQueue,
124             NotificationStackScrollLayoutController stackScrollerController,
125             NotificationPanelViewController notificationPanelViewController,
126             HeadsUpStatusBarView headsUpStatusBarView,
127             View clockView,
128             View operatorNameView,
129             View centeredIconView) {
130         super(headsUpStatusBarView);
131         mNotificationIconAreaController = notificationIconAreaController;
132         mHeadsUpManager = headsUpManager;
133         mCenteredIconView = centeredIconView;
134 
135         // We may be mid-HUN-expansion when this controller is re-created (for example, if the user
136         // has started pulling down the notification shade from the HUN and then the font size
137         // changes). We need to re-fetch these values since they're used to correctly display the
138         // HUN during this shade expansion.
139         mTrackedChild = notificationPanelViewController.getTrackedHeadsUpNotification();
140         mAppearFraction = stackScrollerController.getAppearFraction();
141         mExpandedHeight = stackScrollerController.getExpandedHeight();
142 
143         mStackScrollerController = stackScrollerController;
144         mNotificationPanelViewController = notificationPanelViewController;
145         mStackScrollerController.setHeadsUpAppearanceController(this);
146         mClockView = clockView;
147         mOperatorNameView = operatorNameView;
148         mDarkIconDispatcher = Dependency.get(DarkIconDispatcher.class);
149 
150         mView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
151             @Override
152             public void onLayoutChange(View v, int left, int top, int right, int bottom,
153                     int oldLeft, int oldTop, int oldRight, int oldBottom) {
154                 if (shouldBeVisible()) {
155                     updateTopEntry();
156 
157                     // trigger scroller to notify the latest panel translation
158                     mStackScrollerController.requestLayout();
159                 }
160                 mView.removeOnLayoutChangeListener(this);
161             }
162         });
163         mBypassController = bypassController;
164         mStatusBarStateController = stateController;
165         mWakeUpCoordinator = wakeUpCoordinator;
166         mCommandQueue = commandQueue;
167         mKeyguardStateController = keyguardStateController;
168     }
169 
170     @Override
onViewAttached()171     protected void onViewAttached() {
172         mHeadsUpManager.addListener(this);
173         mView.setOnDrawingRectChangedListener(
174                 () -> updateIsolatedIconLocation(true /* requireUpdate */));
175         mWakeUpCoordinator.addListener(this);
176         mNotificationPanelViewController.addTrackingHeadsUpListener(mSetTrackingHeadsUp);
177         mNotificationPanelViewController.setHeadsUpAppearanceController(this);
178         mStackScrollerController.addOnExpandedHeightChangedListener(mSetExpandedHeight);
179         mDarkIconDispatcher.addDarkReceiver(this);
180     }
181 
182     @Override
onViewDetached()183     protected void onViewDetached() {
184         mHeadsUpManager.removeListener(this);
185         mView.setOnDrawingRectChangedListener(null);
186         mWakeUpCoordinator.removeListener(this);
187         mNotificationPanelViewController.removeTrackingHeadsUpListener(mSetTrackingHeadsUp);
188         mNotificationPanelViewController.setHeadsUpAppearanceController(null);
189         mStackScrollerController.removeOnExpandedHeightChangedListener(mSetExpandedHeight);
190         mDarkIconDispatcher.removeDarkReceiver(this);
191     }
192 
updateIsolatedIconLocation(boolean requireStateUpdate)193     private void updateIsolatedIconLocation(boolean requireStateUpdate) {
194         mNotificationIconAreaController.setIsolatedIconLocation(
195                 mView.getIconDrawingRect(), requireStateUpdate);
196     }
197 
198     @Override
onHeadsUpPinned(NotificationEntry entry)199     public void onHeadsUpPinned(NotificationEntry entry) {
200         updateTopEntry();
201         updateHeader(entry);
202     }
203 
updateTopEntry()204     private void updateTopEntry() {
205         NotificationEntry newEntry = null;
206         if (shouldBeVisible()) {
207             newEntry = mHeadsUpManager.getTopEntry();
208         }
209         NotificationEntry previousEntry = mView.getShowingEntry();
210         mView.setEntry(newEntry);
211         if (newEntry != previousEntry) {
212             boolean animateIsolation = false;
213             if (newEntry == null) {
214                 // no heads up anymore, lets start the disappear animation
215 
216                 setShown(false);
217                 animateIsolation = !isExpanded();
218             } else if (previousEntry == null) {
219                 // We now have a headsUp and didn't have one before. Let's start the disappear
220                 // animation
221                 setShown(true);
222                 animateIsolation = !isExpanded();
223             }
224             updateIsolatedIconLocation(false /* requireUpdate */);
225             mNotificationIconAreaController.showIconIsolated(newEntry == null ? null
226                     : newEntry.getIcons().getStatusBarIcon(), animateIsolation);
227         }
228     }
229 
setShown(boolean isShown)230     private void setShown(boolean isShown) {
231         if (mShown != isShown) {
232             mShown = isShown;
233             if (isShown) {
234                 updateParentClipping(false /* shouldClip */);
235                 mView.setVisibility(View.VISIBLE);
236                 show(mView);
237                 hide(mClockView, View.INVISIBLE);
238                 if (mCenteredIconView.getVisibility() != View.GONE) {
239                     hide(mCenteredIconView, View.INVISIBLE);
240                 }
241                 if (mOperatorNameView != null) {
242                     hide(mOperatorNameView, View.INVISIBLE);
243                 }
244             } else {
245                 show(mClockView);
246                 if (mCenteredIconView.getVisibility() != View.GONE) {
247                     show(mCenteredIconView);
248                 }
249                 if (mOperatorNameView != null) {
250                     show(mOperatorNameView);
251                 }
252                 hide(mView, View.GONE, () -> {
253                     updateParentClipping(true /* shouldClip */);
254                 });
255             }
256             // Show the status bar icons when the view gets shown / hidden
257             if (mStatusBarStateController.getState() != StatusBarState.SHADE) {
258                 mCommandQueue.recomputeDisableFlags(
259                         mView.getContext().getDisplayId(), false);
260             }
261         }
262     }
263 
updateParentClipping(boolean shouldClip)264     private void updateParentClipping(boolean shouldClip) {
265         ViewClippingUtil.setClippingDeactivated(
266                 mView, !shouldClip, mParentClippingParams);
267     }
268 
269     /**
270      * Hides the view and sets the state to endState when finished.
271      *
272      * @param view The view to hide.
273      * @param endState One of {@link View#INVISIBLE} or {@link View#GONE}.
274      * @see HeadsUpAppearanceController#hide(View, int, Runnable)
275      * @see View#setVisibility(int)
276      *
277      */
hide(View view, int endState)278     private void hide(View view, int endState) {
279         hide(view, endState, null);
280     }
281 
282     /**
283      * Hides the view and sets the state to endState when finished.
284      *
285      * @param view The view to hide.
286      * @param endState One of {@link View#INVISIBLE} or {@link View#GONE}.
287      * @param callback Runnable to be executed after the view has been hidden.
288      * @see View#setVisibility(int)
289      *
290      */
hide(View view, int endState, Runnable callback)291     private void hide(View view, int endState, Runnable callback) {
292         if (mAnimationsEnabled) {
293             CrossFadeHelper.fadeOut(view, CONTENT_FADE_DURATION /* duration */,
294                     0 /* delay */, () -> {
295                         view.setVisibility(endState);
296                         if (callback != null) {
297                             callback.run();
298                         }
299                     });
300         } else {
301             view.setVisibility(endState);
302             if (callback != null) {
303                 callback.run();
304             }
305         }
306     }
307 
show(View view)308     private void show(View view) {
309         if (mAnimationsEnabled) {
310             CrossFadeHelper.fadeIn(view, CONTENT_FADE_DURATION /* duration */,
311                     CONTENT_FADE_DELAY /* delay */);
312         } else {
313             view.setVisibility(View.VISIBLE);
314         }
315     }
316 
317     @VisibleForTesting
setAnimationsEnabled(boolean enabled)318     void setAnimationsEnabled(boolean enabled) {
319         mAnimationsEnabled = enabled;
320     }
321 
322     @VisibleForTesting
isShown()323     public boolean isShown() {
324         return mShown;
325     }
326 
327     /**
328      * Should the headsup status bar view be visible right now? This may be different from isShown,
329      * since the headsUp manager might not have notified us yet of the state change.
330      *
331      * @return if the heads up status bar view should be shown
332      */
shouldBeVisible()333     public boolean shouldBeVisible() {
334         boolean notificationsShown = !mWakeUpCoordinator.getNotificationsFullyHidden();
335         boolean canShow = !isExpanded() && notificationsShown;
336         if (mBypassController.getBypassEnabled() &&
337                 (mStatusBarStateController.getState() == StatusBarState.KEYGUARD
338                         || mKeyguardStateController.isKeyguardGoingAway())
339                 && notificationsShown) {
340             canShow = true;
341         }
342         return canShow && mHeadsUpManager.hasPinnedHeadsUp();
343     }
344 
345     @Override
onHeadsUpUnPinned(NotificationEntry entry)346     public void onHeadsUpUnPinned(NotificationEntry entry) {
347         updateTopEntry();
348         updateHeader(entry);
349     }
350 
setAppearFraction(float expandedHeight, float appearFraction)351     public void setAppearFraction(float expandedHeight, float appearFraction) {
352         boolean changed = expandedHeight != mExpandedHeight;
353         boolean oldIsExpanded = isExpanded();
354 
355         mExpandedHeight = expandedHeight;
356         mAppearFraction = appearFraction;
357         // We only notify if the expandedHeight changed and not on the appearFraction, since
358         // otherwise we may run into an infinite loop where the panel and this are constantly
359         // updating themselves over just a small fraction
360         if (changed) {
361             updateHeadsUpHeaders();
362         }
363         if (isExpanded() != oldIsExpanded) {
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 
isExpanded()383     private boolean isExpanded() {
384         return mExpandedHeight > 0;
385     }
386 
updateHeadsUpHeaders()387     private void updateHeadsUpHeaders() {
388         mHeadsUpManager.getAllEntries().forEach(entry -> {
389             updateHeader(entry);
390         });
391     }
392 
updateHeader(NotificationEntry entry)393     public void updateHeader(NotificationEntry entry) {
394         ExpandableNotificationRow row = entry.getRow();
395         float headerVisibleAmount = 1.0f;
396         if (row.isPinned() || row.isHeadsUpAnimatingAway() || row == mTrackedChild
397                 || row.showingPulsing()) {
398             headerVisibleAmount = mAppearFraction;
399         }
400         row.setHeaderVisibleAmount(headerVisibleAmount);
401     }
402 
403     @Override
onDarkChanged(Rect area, float darkIntensity, int tint)404     public void onDarkChanged(Rect area, float darkIntensity, int tint) {
405         mView.onDarkChanged(area, darkIntensity, tint);
406     }
407 
onStateChanged()408     public void onStateChanged() {
409         updateTopEntry();
410     }
411 
412     @Override
onFullyHiddenChanged(boolean isFullyHidden)413     public void onFullyHiddenChanged(boolean isFullyHidden) {
414         updateTopEntry();
415     }
416 }
417