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