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