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