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