1 /*
2  * Copyright (C) 2023 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.qs;
18 
19 import static android.app.StatusBarManager.DISABLE2_QUICK_SETTINGS;
20 
21 import static com.android.systemui.media.dagger.MediaModule.QS_PANEL;
22 import static com.android.systemui.media.dagger.MediaModule.QUICK_QS_PANEL;
23 import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;
24 import static com.android.systemui.statusbar.StatusBarState.SHADE_LOCKED;
25 
26 import android.animation.Animator;
27 import android.animation.AnimatorListenerAdapter;
28 import android.content.Context;
29 import android.content.res.Configuration;
30 import android.content.res.Resources;
31 import android.graphics.Rect;
32 import android.os.Bundle;
33 import android.util.IndentingPrintWriter;
34 import android.util.Log;
35 import android.view.View;
36 import android.view.ViewGroup;
37 
38 import androidx.annotation.FloatRange;
39 import androidx.annotation.Nullable;
40 import androidx.annotation.VisibleForTesting;
41 import androidx.compose.ui.platform.ComposeView;
42 import androidx.lifecycle.Lifecycle;
43 import androidx.lifecycle.LifecycleOwner;
44 import androidx.lifecycle.LifecycleRegistry;
45 
46 import com.android.app.animation.Interpolators;
47 import com.android.keyguard.BouncerPanelExpansionCalculator;
48 import com.android.systemui.Dumpable;
49 import com.android.systemui.animation.ShadeInterpolation;
50 import com.android.systemui.dump.DumpManager;
51 import com.android.systemui.media.controls.ui.view.MediaHost;
52 import com.android.systemui.plugins.qs.QS;
53 import com.android.systemui.plugins.qs.QSContainerController;
54 import com.android.systemui.plugins.statusbar.StatusBarStateController;
55 import com.android.systemui.qs.customize.QSCustomizerController;
56 import com.android.systemui.qs.dagger.QSComponent;
57 import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel;
58 import com.android.systemui.qs.logging.QSLogger;
59 import com.android.systemui.res.R;
60 import com.android.systemui.scene.shared.flag.SceneContainerFlag;
61 import com.android.systemui.settings.brightness.MirrorController;
62 import com.android.systemui.shade.transition.LargeScreenShadeInterpolator;
63 import com.android.systemui.statusbar.CommandQueue;
64 import com.android.systemui.statusbar.StatusBarState;
65 import com.android.systemui.statusbar.SysuiStatusBarStateController;
66 import com.android.systemui.statusbar.disableflags.DisableFlagsLogger;
67 import com.android.systemui.statusbar.notification.stack.StackStateAnimator;
68 import com.android.systemui.statusbar.phone.KeyguardBypassController;
69 import com.android.systemui.statusbar.policy.RemoteInputQuickSettingsDisabler;
70 import com.android.systemui.util.Utils;
71 
72 import dalvik.annotation.optimization.NeverCompile;
73 
74 import java.io.PrintWriter;
75 import java.util.Arrays;
76 import java.util.function.Consumer;
77 
78 import javax.inject.Inject;
79 import javax.inject.Named;
80 
81 public class QSImpl implements QS, CommandQueue.Callbacks, StatusBarStateController.StateListener,
82         Dumpable {
83     private static final String TAG = "QS";
84     private static final boolean DEBUG = false;
85     private static final String EXTRA_EXPANDED = "expanded";
86     private static final String EXTRA_LISTENING = "listening";
87     private static final String EXTRA_VISIBLE = "visible";
88 
89     private final Rect mQsBounds = new Rect();
90     private final SysuiStatusBarStateController mStatusBarStateController;
91     private final KeyguardBypassController mBypassController;
92     private boolean mQsExpanded;
93     private boolean mHeaderAnimating;
94     private boolean mStackScrollerOverscrolling;
95 
96     private QSAnimator mQSAnimator;
97     @Nullable
98     private HeightListener mPanelView;
99     private QSSquishinessController mQSSquishinessController;
100     protected QuickStatusBarHeader mHeader;
101     protected NonInterceptingScrollView mQSPanelScrollView;
102     private boolean mListening;
103     private QSContainerImpl mContainer;
104     private int mLayoutDirection;
105     private QSFooter mFooter;
106     private float mLastQSExpansion = -1;
107     private float mLastPanelFraction;
108     private float mSquishinessFraction = 1;
109     private boolean mQsDisabled;
110     private int[] mLocationTemp = new int[2];
111 
112     private final RemoteInputQuickSettingsDisabler mRemoteInputQuickSettingsDisabler;
113     private final MediaHost mQsMediaHost;
114     private final MediaHost mQqsMediaHost;
115     private final QSDisableFlagsLogger mQsDisableFlagsLogger;
116     private final LargeScreenShadeInterpolator mLargeScreenShadeInterpolator;
117     private final QSLogger mLogger;
118     private final FooterActionsController mFooterActionsController;
119     private final FooterActionsViewModel.Factory mFooterActionsViewModelFactory;
120     private final ListeningAndVisibilityLifecycleOwner mListeningAndVisibilityLifecycleOwner;
121     private boolean mShowCollapsedOnKeyguard;
122     private boolean mLastKeyguardAndExpanded;
123     /**
124      * The last received state from the controller. This should not be used directly to check if
125      * we're on keyguard but use {@link #isKeyguardState()} instead since that is more accurate
126      * during state transitions which often call into us.
127      */
128     private int mStatusBarState = -1;
129     private QSContainerImplController mQSContainerImplController;
130     private int[] mTmpLocation = new int[2];
131     private int mLastViewHeight;
132     private float mLastHeaderTranslation;
133     private QSPanelController mQSPanelController;
134     private QuickQSPanelController mQuickQSPanelController;
135     private QSCustomizerController mQSCustomizerController;
136     private FooterActionsViewModel mQSFooterActionsViewModel;
137     @Nullable
138     private ScrollListener mScrollListener;
139     /**
140      * When true, QS will translate from outside the screen. It will be clipped with parallax
141      * otherwise.
142      */
143     private boolean mInSplitShade;
144 
145     /**
146      * Are we currently transitioning from lockscreen to the full shade?
147      */
148     private boolean mTransitioningToFullShade;
149 
150     private final DumpManager mDumpManager;
151 
152     /**
153      * Progress of pull down from the center of the lock screen.
154      * @see com.android.systemui.statusbar.LockscreenShadeTransitionController
155      */
156     private float mLockscreenToShadeProgress;
157 
158     private boolean mOverScrolling;
159 
160     // Whether QQS or QS is visible. When in lockscreen, this is true if and only if QQS or QS is
161     // visible;
162     private boolean mQsVisible;
163 
164     private boolean mIsSmallScreen;
165 
166     /** Should the squishiness fraction be updated on the media host. */
167     private boolean mShouldUpdateMediaSquishiness;
168 
169     private CommandQueue mCommandQueue;
170 
171     private View mRootView;
172     @Nullable
173     private ComposeView mFooterActionsView;
174 
175     @Inject
QSImpl(RemoteInputQuickSettingsDisabler remoteInputQsDisabler, SysuiStatusBarStateController statusBarStateController, CommandQueue commandQueue, @Named(QS_PANEL) MediaHost qsMediaHost, @Named(QUICK_QS_PANEL) MediaHost qqsMediaHost, KeyguardBypassController keyguardBypassController, QSDisableFlagsLogger qsDisableFlagsLogger, DumpManager dumpManager, QSLogger qsLogger, FooterActionsController footerActionsController, FooterActionsViewModel.Factory footerActionsViewModelFactory, LargeScreenShadeInterpolator largeScreenShadeInterpolator)176     public QSImpl(RemoteInputQuickSettingsDisabler remoteInputQsDisabler,
177             SysuiStatusBarStateController statusBarStateController, CommandQueue commandQueue,
178             @Named(QS_PANEL) MediaHost qsMediaHost,
179             @Named(QUICK_QS_PANEL) MediaHost qqsMediaHost,
180             KeyguardBypassController keyguardBypassController,
181             QSDisableFlagsLogger qsDisableFlagsLogger,
182             DumpManager dumpManager, QSLogger qsLogger,
183             FooterActionsController footerActionsController,
184             FooterActionsViewModel.Factory footerActionsViewModelFactory,
185             LargeScreenShadeInterpolator largeScreenShadeInterpolator) {
186         mRemoteInputQuickSettingsDisabler = remoteInputQsDisabler;
187         mQsMediaHost = qsMediaHost;
188         mQqsMediaHost = qqsMediaHost;
189         mQsDisableFlagsLogger = qsDisableFlagsLogger;
190         mLogger = qsLogger;
191         mLargeScreenShadeInterpolator = largeScreenShadeInterpolator;
192         mCommandQueue = commandQueue;
193         mBypassController = keyguardBypassController;
194         mStatusBarStateController = statusBarStateController;
195         mDumpManager = dumpManager;
196         mFooterActionsController = footerActionsController;
197         mFooterActionsViewModelFactory = footerActionsViewModelFactory;
198         mListeningAndVisibilityLifecycleOwner = new ListeningAndVisibilityLifecycleOwner();
199         if (SceneContainerFlag.isEnabled()) {
200             mStatusBarState = StatusBarState.SHADE;
201         }
202     }
203 
204     /**
205      * This method will set up all the necessary fields. Methods from the implemented interfaces
206      * should not be called before this method returns.
207      */
onComponentCreated(QSComponent qsComponent, @Nullable Bundle savedInstanceState)208     public void onComponentCreated(QSComponent qsComponent, @Nullable Bundle savedInstanceState) {
209         mRootView = qsComponent.getRootView();
210 
211         mQSPanelController = qsComponent.getQSPanelController();
212         mQuickQSPanelController = qsComponent.getQuickQSPanelController();
213 
214         mQSPanelController.init();
215         mQuickQSPanelController.init();
216 
217         if (!SceneContainerFlag.isEnabled()) {
218             mQSFooterActionsViewModel = mFooterActionsViewModelFactory
219                     .create(mListeningAndVisibilityLifecycleOwner);
220             bindFooterActionsView(mRootView);
221             mFooterActionsController.init();
222         } else {
223             View footerView = mRootView.findViewById(R.id.qs_footer_actions);
224             if (footerView != null) {
225                 ((ViewGroup) footerView.getParent()).removeView(footerView);
226             }
227         }
228 
229         mQSPanelScrollView = mRootView.findViewById(R.id.expanded_qs_scroll_view);
230         mQSPanelScrollView.addOnLayoutChangeListener(
231                 (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
232                     updateQsBounds();
233                 });
234         mQSPanelScrollView.setOnScrollChangeListener(
235                 (v, scrollX, scrollY, oldScrollX, oldScrollY) -> {
236                     // Lazily update animators whenever the scrolling changes
237                     mQSAnimator.requestAnimatorUpdate();
238                     if (mScrollListener != null) {
239                         mScrollListener.onQsPanelScrollChanged(scrollY);
240                     }
241                 });
242         mQSPanelScrollView.setScrollingEnabled(!SceneContainerFlag.isEnabled());
243         mHeader = mRootView.findViewById(R.id.header);
244         mFooter = qsComponent.getQSFooter();
245 
246         mQSContainerImplController = qsComponent.getQSContainerImplController();
247         mQSContainerImplController.init();
248         mContainer = mQSContainerImplController.getView();
249         mDumpManager.registerDumpable(mContainer.getClass().getSimpleName(), mContainer);
250 
251         mQSAnimator = qsComponent.getQSAnimator();
252         mQSSquishinessController = qsComponent.getQSSquishinessController();
253 
254         mQSCustomizerController = qsComponent.getQSCustomizerController();
255         mQSCustomizerController.init();
256         mQSCustomizerController.setQs(this);
257         if (savedInstanceState != null) {
258             setQsVisible(savedInstanceState.getBoolean(EXTRA_VISIBLE));
259             setExpanded(savedInstanceState.getBoolean(EXTRA_EXPANDED));
260             setListening(savedInstanceState.getBoolean(EXTRA_LISTENING));
261             setEditLocation(mRootView);
262             mQSCustomizerController.restoreInstanceState(savedInstanceState);
263             if (mQsExpanded) {
264                 mQSPanelController.getTileLayout().restoreInstanceState(savedInstanceState);
265             }
266         }
267         mStatusBarStateController.addCallback(this);
268         onStateChanged(mStatusBarStateController.getState());
269         mRootView.addOnLayoutChangeListener(
270                 (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
271                     boolean sizeChanged = (oldTop - oldBottom) != (top - bottom);
272                     if (sizeChanged) {
273                         setQsExpansion(mLastQSExpansion, mLastPanelFraction,
274                                 mLastHeaderTranslation, mSquishinessFraction);
275                     }
276                 });
277         mQSPanelController.setUsingHorizontalLayoutChangeListener(
278                 () -> {
279                     // The hostview may be faded out in the horizontal layout. Let's make sure to
280                     // reset the alpha when switching layouts. This is fine since the animator will
281                     // update the alpha if it's not supposed to be 1.0f
282                     mQSPanelController.getMediaHost().getHostView().setAlpha(1.0f);
283                     mQSAnimator.requestAnimatorUpdate();
284                 });
285 
286         // This will immediately call disable, so it needs to be added after setting up the fields.
287         mCommandQueue.addCallback(this);
288     }
289 
bindFooterActionsView(View root)290     private void bindFooterActionsView(View root) {
291         mFooterActionsView = root.findViewById(R.id.qs_footer_actions);
292         QSUtils.setFooterActionsViewContent(mFooterActionsView,
293                 mQSFooterActionsViewModel, mListeningAndVisibilityLifecycleOwner);
294     }
295 
296     @Override
setScrollListener(ScrollListener listener)297     public void setScrollListener(ScrollListener listener) {
298         mScrollListener = listener;
299     }
300 
onCreate(Bundle savedInstanceState)301     public void onCreate(Bundle savedInstanceState) {
302         mDumpManager.registerDumpable(getClass().getSimpleName(), this);
303     }
304 
onDestroy()305     public void onDestroy() {
306         mCommandQueue.removeCallback(this);
307         mStatusBarStateController.removeCallback(this);
308         mQSPanelController.destroy();
309         mQuickQSPanelController.destroy();
310         if (mListening) {
311             setListening(false);
312         }
313         if (mQSCustomizerController != null) {
314             mQSCustomizerController.setQs(null);
315             mQSCustomizerController.setContainerController(null);
316         }
317         mScrollListener = null;
318         if (mContainer != null) {
319             mDumpManager.unregisterDumpable(mContainer.getClass().getSimpleName());
320         }
321         mDumpManager.unregisterDumpable(getClass().getSimpleName());
322         mListeningAndVisibilityLifecycleOwner.destroy();
323         ViewGroup parent = ((ViewGroup) getView().getParent());
324         if (parent != null) {
325             parent.removeView(getView());
326         }
327     }
328 
onSaveInstanceState(Bundle outState)329     public void onSaveInstanceState(Bundle outState) {
330         outState.putBoolean(EXTRA_EXPANDED, mQsExpanded);
331         outState.putBoolean(EXTRA_LISTENING, mListening);
332         outState.putBoolean(EXTRA_VISIBLE, mQsVisible);
333         if (mQSCustomizerController != null) {
334             mQSCustomizerController.saveInstanceState(outState);
335         }
336         if (mQsExpanded) {
337             mQSPanelController.getTileLayout().saveInstanceState(outState);
338         }
339     }
340 
341     @VisibleForTesting
isListening()342     boolean isListening() {
343         return mListening;
344     }
345 
346     @VisibleForTesting
isExpanded()347     boolean isExpanded() {
348         return mQsExpanded;
349     }
350 
351     @VisibleForTesting
isQsVisible()352     boolean isQsVisible() {
353         return mQsVisible;
354     }
355 
356     @Override
getHeader()357     public View getHeader() {
358         return mHeader;
359     }
360 
361     @Override
setHasNotifications(boolean hasNotifications)362     public void setHasNotifications(boolean hasNotifications) {
363     }
364 
365     @Override
setPanelView(HeightListener panelView)366     public void setPanelView(HeightListener panelView) {
367         mPanelView = panelView;
368     }
369 
onConfigurationChanged(Configuration newConfig)370     public void onConfigurationChanged(Configuration newConfig) {
371         setEditLocation(getView());
372         if (newConfig.getLayoutDirection() != mLayoutDirection) {
373             mLayoutDirection = newConfig.getLayoutDirection();
374             if (mQSAnimator != null) {
375                 mQSAnimator.onRtlChanged();
376             }
377         }
378         updateQsState();
379     }
380 
381     @Override
setFancyClipping(int leftInset, int top, int rightInset, int bottom, int cornerRadius, boolean visible, boolean fullWidth)382     public void setFancyClipping(int leftInset, int top, int rightInset, int bottom,
383             int cornerRadius, boolean visible, boolean fullWidth) {
384         if (getView() instanceof QSContainerImpl) {
385             ((QSContainerImpl) getView()).setFancyClipping(leftInset, top, rightInset, bottom,
386                     cornerRadius, visible, fullWidth);
387         }
388     }
389 
390     @Override
isFullyCollapsed()391     public boolean isFullyCollapsed() {
392         return mLastQSExpansion == 0.0f || mLastQSExpansion == -1;
393     }
394 
395     @Override
setCollapsedMediaVisibilityChangedListener(Consumer<Boolean> listener)396     public void setCollapsedMediaVisibilityChangedListener(Consumer<Boolean> listener) {
397         mQuickQSPanelController.setMediaVisibilityChangedListener(listener);
398     }
399 
setEditLocation(View view)400     private void setEditLocation(View view) {
401         View edit = view.findViewById(android.R.id.edit);
402         int[] loc = edit.getLocationOnScreen();
403         int x = loc[0] + edit.getWidth() / 2;
404         int y = loc[1] + edit.getHeight() / 2;
405         mQSCustomizerController.setEditLocation(x, y);
406     }
407 
408     @Override
setContainerController(QSContainerController controller)409     public void setContainerController(QSContainerController controller) {
410         mQSCustomizerController.setContainerController(controller);
411     }
412 
413     @Override
isCustomizing()414     public boolean isCustomizing() {
415         return mQSCustomizerController.isCustomizing();
416     }
417 
418     @Override
disable(int displayId, int state1, int state2, boolean animate)419     public void disable(int displayId, int state1, int state2, boolean animate) {
420         if (displayId != getContext().getDisplayId()) {
421             return;
422         }
423         int state2BeforeAdjustment = state2;
424         state2 = mRemoteInputQuickSettingsDisabler.adjustDisableFlags(state2);
425 
426         mQsDisableFlagsLogger.logDisableFlagChange(
427                 /* new= */ new DisableFlagsLogger.DisableState(state1, state2BeforeAdjustment),
428                 /* newAfterLocalModification= */ new DisableFlagsLogger.DisableState(state1, state2)
429         );
430 
431         final boolean disabled = (state2 & DISABLE2_QUICK_SETTINGS) != 0;
432         if (disabled == mQsDisabled) return;
433         mQsDisabled = disabled;
434         mContainer.disable(state1, state2, animate);
435         mHeader.disable(state1, state2, animate);
436         mFooter.disable(state1, state2, animate);
437         updateQsState();
438     }
439 
updateQsState()440     private void updateQsState() {
441         final boolean expandVisually = mQsExpanded || mStackScrollerOverscrolling
442                 || mHeaderAnimating;
443         mQSPanelController.setExpanded(mQsExpanded);
444         boolean keyguardShowing = isKeyguardState();
445         mHeader.setVisibility((mQsExpanded || !keyguardShowing || mHeaderAnimating
446                 || mShowCollapsedOnKeyguard)
447                 ? View.VISIBLE
448                 : View.INVISIBLE);
449         mHeader.setExpanded((keyguardShowing && !mHeaderAnimating && !mShowCollapsedOnKeyguard)
450                 || (mQsExpanded && !mStackScrollerOverscrolling), mQuickQSPanelController);
451         boolean qsPanelVisible = !mQsDisabled && expandVisually;
452         boolean footerVisible = qsPanelVisible && (mQsExpanded || !keyguardShowing
453                 || mHeaderAnimating || mShowCollapsedOnKeyguard);
454         mFooter.setVisibility(footerVisible ? View.VISIBLE : View.INVISIBLE);
455         if (mFooterActionsView != null) {
456             mFooterActionsView.setVisibility(footerVisible ? View.VISIBLE : View.INVISIBLE);
457         }
458         mFooter.setExpanded((keyguardShowing && !mHeaderAnimating && !mShowCollapsedOnKeyguard)
459                 || (mQsExpanded && !mStackScrollerOverscrolling));
460         mQSPanelController.setVisibility(qsPanelVisible ? View.VISIBLE : View.INVISIBLE);
461         if (DEBUG) {
462             Log.d(TAG, "Footer: " + footerVisible + ", QS Panel: " + qsPanelVisible);
463         }
464     }
465 
466     @VisibleForTesting
isKeyguardState()467     boolean isKeyguardState() {
468         if (SceneContainerFlag.isEnabled()) {
469             return false;
470         } else {
471             // We want the freshest state here since otherwise we'll have some weirdness if earlier
472             // listeners trigger updates
473             return mStatusBarStateController.getCurrentOrUpcomingState() == KEYGUARD;
474         }
475     }
476 
477     @VisibleForTesting
getStatusBarState()478     int getStatusBarState() {
479         return mStatusBarState;
480     }
481 
updateShowCollapsedOnKeyguard()482     private void updateShowCollapsedOnKeyguard() {
483         boolean showCollapsed = mBypassController.getBypassEnabled()
484                 || (mTransitioningToFullShade && !mInSplitShade);
485         if (showCollapsed != mShowCollapsedOnKeyguard) {
486             mShowCollapsedOnKeyguard = showCollapsed;
487             updateQsState();
488             if (mQSAnimator != null) {
489                 mQSAnimator.setShowCollapsedOnKeyguard(showCollapsed);
490             }
491             if (!showCollapsed && isKeyguardState()) {
492                 setQsExpansion(mLastQSExpansion, mLastPanelFraction, 0,
493                         mSquishinessFraction);
494             }
495         }
496     }
497 
getQSPanelController()498     public QSPanelController getQSPanelController() {
499         return mQSPanelController;
500     }
501 
setBrightnessMirrorController( @ullable MirrorController brightnessMirrorController)502     public void setBrightnessMirrorController(
503             @Nullable MirrorController brightnessMirrorController) {
504         mQSPanelController.setBrightnessMirror(brightnessMirrorController);
505     }
506 
507     @Override
isShowingDetail()508     public boolean isShowingDetail() {
509         return mQSCustomizerController.isCustomizing();
510     }
511 
512     @Override
setHeaderClickable(boolean clickable)513     public void setHeaderClickable(boolean clickable) {
514         if (DEBUG) Log.d(TAG, "setHeaderClickable " + clickable);
515     }
516 
517     @Override
setExpanded(boolean expanded)518     public void setExpanded(boolean expanded) {
519         if (DEBUG) Log.d(TAG, "setExpanded " + expanded);
520         mQsExpanded = expanded;
521         if (mInSplitShade && mQsExpanded) {
522             // in split shade QS is expanded immediately when shade expansion starts and then we
523             // also need to listen to changes - otherwise QS is updated only once its fully expanded
524             setListening(true);
525         } else {
526             updateQsPanelControllerListening();
527         }
528         updateQsState();
529     }
530 
setKeyguardShowing(boolean keyguardShowing)531     private void setKeyguardShowing(boolean keyguardShowing) {
532         if (!SceneContainerFlag.isEnabled()) {
533             if (DEBUG) Log.d(TAG, "setKeyguardShowing " + keyguardShowing);
534             mLastQSExpansion = -1;
535 
536             if (mQSAnimator != null) {
537                 mQSAnimator.setOnKeyguard(keyguardShowing);
538             }
539 
540             mFooter.setKeyguardShowing(keyguardShowing);
541             updateQsState();
542         }
543     }
544 
545     @Override
setOverscrolling(boolean stackScrollerOverscrolling)546     public void setOverscrolling(boolean stackScrollerOverscrolling) {
547         if (DEBUG) Log.d(TAG, "setOverscrolling " + stackScrollerOverscrolling);
548         mStackScrollerOverscrolling = stackScrollerOverscrolling;
549         updateQsState();
550     }
551 
552     @Override
setListening(boolean listening)553     public void setListening(boolean listening) {
554         if (DEBUG) Log.d(TAG, "setListening " + listening);
555         mListening = listening;
556         mQSContainerImplController.setListening(listening && mQsVisible);
557         mListeningAndVisibilityLifecycleOwner.updateState();
558         updateQsPanelControllerListening();
559     }
560 
updateQsPanelControllerListening()561     private void updateQsPanelControllerListening() {
562         mQSPanelController.setListening(mListening && mQsVisible, mQsExpanded);
563     }
564 
565     @Override
setQsVisible(boolean visible)566     public void setQsVisible(boolean visible) {
567         if (DEBUG) Log.d(TAG, "setQsVisible " + visible);
568         mQsVisible = visible;
569         setListening(mListening);
570         mListeningAndVisibilityLifecycleOwner.updateState();
571     }
572 
573     @Override
setHeaderListening(boolean listening)574     public void setHeaderListening(boolean listening) {
575         mQSContainerImplController.setListening(listening);
576     }
577 
578     @Override
setInSplitShade(boolean inSplitShade)579     public void setInSplitShade(boolean inSplitShade) {
580         mInSplitShade = inSplitShade;
581         updateShowCollapsedOnKeyguard();
582         updateQsState();
583     }
584 
585     @Override
setTransitionToFullShadeProgress( boolean isTransitioningToFullShade, @FloatRange(from = 0.0, to = 1.0) float qsTransitionFraction, @FloatRange(from = 0.0, to = 1.0) float qsSquishinessFraction)586     public void setTransitionToFullShadeProgress(
587             boolean isTransitioningToFullShade,
588             @FloatRange(from = 0.0, to = 1.0) float qsTransitionFraction,
589             @FloatRange(from = 0.0, to = 1.0) float qsSquishinessFraction) {
590         if (isTransitioningToFullShade != mTransitioningToFullShade) {
591             mTransitioningToFullShade = isTransitioningToFullShade;
592             updateShowCollapsedOnKeyguard();
593         }
594         mLockscreenToShadeProgress = qsTransitionFraction;
595         setQsExpansion(mLastQSExpansion, mLastPanelFraction, mLastHeaderTranslation,
596                 isTransitioningToFullShade ? qsSquishinessFraction : mSquishinessFraction);
597     }
598 
599     @Override
setOverScrollAmount(int overScrollAmount)600     public void setOverScrollAmount(int overScrollAmount) {
601         mOverScrolling = overScrollAmount != 0;
602         View view = getView();
603         if (view != null) {
604             view.setTranslationY(overScrollAmount);
605         }
606     }
607 
608     @Override
getHeightDiff()609     public int getHeightDiff() {
610         if (SceneContainerFlag.isEnabled()) {
611             return mQSPanelController.getViewBottom() - mHeader.getBottom()
612                     + mHeader.getPaddingBottom();
613         } else {
614             return mQSPanelScrollView.getBottom() - mHeader.getBottom()
615                     + mHeader.getPaddingBottom();
616         }
617     }
618 
619     @Override
setIsNotificationPanelFullWidth(boolean isFullWidth)620     public void setIsNotificationPanelFullWidth(boolean isFullWidth) {
621         mIsSmallScreen = isFullWidth;
622     }
623 
624     @Override
setShouldUpdateSquishinessOnMedia(boolean shouldUpdate)625     public void setShouldUpdateSquishinessOnMedia(boolean shouldUpdate) {
626         if (DEBUG) Log.d(TAG, "setShouldUpdateSquishinessOnMedia " + shouldUpdate);
627         mShouldUpdateMediaSquishiness = shouldUpdate;
628     }
629 
630     @Override
setQsExpansion(float expansion, float panelExpansionFraction, float proposedTranslation, float squishinessFraction)631     public void setQsExpansion(float expansion, float panelExpansionFraction,
632             float proposedTranslation, float squishinessFraction) {
633         float headerTranslation = mTransitioningToFullShade ? 0 : proposedTranslation;
634         float alphaProgress = calculateAlphaProgress(panelExpansionFraction);
635         setAlphaAnimationProgress(alphaProgress);
636         mContainer.setExpansion(expansion);
637         final float translationScaleY = (mInSplitShade
638                 ? 1 : QSAnimator.SHORT_PARALLAX_AMOUNT) * (expansion - 1);
639         boolean onKeyguard = isKeyguardState();
640         boolean onKeyguardAndExpanded = onKeyguard && !mShowCollapsedOnKeyguard;
641         if (!mHeaderAnimating && !headerWillBeAnimating() && !mOverScrolling) {
642             getView().setTranslationY(
643                     onKeyguardAndExpanded
644                             ? translationScaleY * mHeader.getHeight()
645                             : headerTranslation);
646         }
647         int currentHeight = getView().getHeight();
648         if (expansion == mLastQSExpansion
649                 && mLastKeyguardAndExpanded == onKeyguardAndExpanded
650                 && mLastViewHeight == currentHeight
651                 && mLastHeaderTranslation == headerTranslation
652                 && mSquishinessFraction == squishinessFraction
653                 && mLastPanelFraction == panelExpansionFraction) {
654             return;
655         }
656         mLastHeaderTranslation = headerTranslation;
657         mLastPanelFraction = panelExpansionFraction;
658         mSquishinessFraction = squishinessFraction;
659         mLastQSExpansion = expansion;
660         mLastKeyguardAndExpanded = onKeyguardAndExpanded;
661         mLastViewHeight = currentHeight;
662 
663         boolean fullyExpanded = expansion == 1;
664         boolean fullyCollapsed = expansion == 0.0f;
665         int heightDiff = getHeightDiff();
666         float panelTranslationY = translationScaleY * heightDiff;
667 
668         if (expansion < 1 && expansion > 0.99) {
669             if (mQuickQSPanelController.switchTileLayout(false)) {
670                 mHeader.updateResources();
671             }
672         }
673         mQSPanelController.setIsOnKeyguard(onKeyguard);
674         mFooter.setExpansion(onKeyguardAndExpanded ? 1 : expansion);
675         float footerActionsExpansion =
676                 onKeyguardAndExpanded ? 1 : mInSplitShade ? alphaProgress : expansion;
677         if (mQSFooterActionsViewModel != null) {
678             mQSFooterActionsViewModel.onQuickSettingsExpansionChanged(footerActionsExpansion,
679                     mInSplitShade);
680         }
681         mQSPanelController.setRevealExpansion(expansion);
682         mQSPanelController.getTileLayout().setExpansion(expansion, proposedTranslation);
683         mQuickQSPanelController.getTileLayout().setExpansion(expansion, proposedTranslation);
684 
685         if (!SceneContainerFlag.isEnabled()) {
686             float qsScrollViewTranslation =
687                     onKeyguard && !mShowCollapsedOnKeyguard ? panelTranslationY : 0;
688             mQSPanelScrollView.setTranslationY(qsScrollViewTranslation);
689 
690             if (fullyCollapsed) {
691                 mQSPanelScrollView.setScrollY(0);
692             }
693 
694             if (!fullyExpanded) {
695                 // Set bounds on the QS panel so it doesn't run over the header when animating.
696                 mQsBounds.top = (int) -mQSPanelScrollView.getTranslationY();
697                 mQsBounds.right = mQSPanelScrollView.getWidth();
698                 mQsBounds.bottom = mQSPanelScrollView.getHeight();
699             }
700         }
701         updateQsBounds();
702 
703         if (mQSSquishinessController != null) {
704             mQSSquishinessController.setSquishiness(mSquishinessFraction);
705         }
706         if (mQSAnimator != null) {
707             mQSAnimator.setPosition(expansion);
708         }
709         if (!mShouldUpdateMediaSquishiness
710                 && (!mInSplitShade
711                 || mStatusBarStateController.getState() == KEYGUARD
712                 || mStatusBarStateController.getState() == SHADE_LOCKED)
713         ) {
714             // At beginning, state is 0 and will apply wrong squishiness to MediaHost in lockscreen
715             // and media player expect no change by squishiness in lock screen shade. Don't bother
716             // squishing mQsMediaHost when not in split shade to prevent problems with stale state.
717             mQsMediaHost.setSquishFraction(1.0F);
718         } else {
719             mQsMediaHost.setSquishFraction(mSquishinessFraction);
720         }
721         updateMediaPositions();
722     }
723 
setAlphaAnimationProgress(float progress)724     private void setAlphaAnimationProgress(float progress) {
725         final View view = getView();
726         if (progress == 0 && view.getVisibility() != View.INVISIBLE) {
727             mLogger.logVisibility("QS fragment", View.INVISIBLE);
728             view.setVisibility(View.INVISIBLE);
729         } else if (progress > 0 && view.getVisibility() != View.VISIBLE) {
730             mLogger.logVisibility("QS fragment", View.VISIBLE);
731             view.setVisibility((View.VISIBLE));
732         }
733         view.setAlpha(interpolateAlphaAnimationProgress(progress));
734     }
735 
calculateAlphaProgress(float panelExpansionFraction)736     private float calculateAlphaProgress(float panelExpansionFraction) {
737         if (mIsSmallScreen) {
738             // Small screens. QS alpha is not animated.
739             return 1;
740         }
741         if (mInSplitShade) {
742             // Large screens in landscape.
743             // Need to check upcoming state as for unlocked -> AOD transition current state is
744             // not updated yet, but we're transitioning and UI should already follow KEYGUARD state
745             if (mTransitioningToFullShade
746                     || mStatusBarStateController.getCurrentOrUpcomingState() == KEYGUARD) {
747                 // Always use "mFullShadeProgress" on keyguard, because
748                 // "panelExpansionFractions" is always 1 on keyguard split shade.
749                 return mLockscreenToShadeProgress;
750             } else {
751                 return panelExpansionFraction;
752             }
753         }
754         // Large screens in portrait.
755         if (mTransitioningToFullShade) {
756             // Only use this value during the standard lock screen shade expansion. During the
757             // "quick" expansion from top, this value is 0.
758             return mLockscreenToShadeProgress;
759         } else {
760             return panelExpansionFraction;
761         }
762     }
763 
interpolateAlphaAnimationProgress(float progress)764     private float interpolateAlphaAnimationProgress(float progress) {
765         if (mQSPanelController.isBouncerInTransit()) {
766             return BouncerPanelExpansionCalculator.aboutToShowBouncerProgress(progress);
767         }
768         if (isKeyguardState()) {
769             // Alpha progress should be linear on lockscreen shade expansion.
770             return progress;
771         }
772         if (mIsSmallScreen) {
773             return ShadeInterpolation.getContentAlpha(progress);
774         } else {
775             return mLargeScreenShadeInterpolator.getQsAlpha(progress);
776         }
777     }
778 
779     @VisibleForTesting
updateQsBounds()780     void updateQsBounds() {
781         if (mLastQSExpansion == 1.0f) {
782             // Fully expanded, let's set the layout bounds as clip bounds. This is necessary because
783             // it's a scrollview and otherwise wouldn't be clipped. However, we set the horizontal
784             // bounds so the pages go to the ends of QSContainerImpl (most cases) or its parent
785             // (large screen portrait)
786             int sideMargin = getResources().getDimensionPixelSize(
787                     R.dimen.qs_tiles_page_horizontal_margin) * 2;
788             mQsBounds.set(-sideMargin, 0, mQSPanelScrollView.getWidth() + sideMargin,
789                     mQSPanelScrollView.getHeight());
790         }
791         if (!SceneContainerFlag.isEnabled()) {
792             mQSPanelScrollView.setClipBounds(mQsBounds);
793 
794             mQSPanelScrollView.getLocationOnScreen(mLocationTemp);
795             int left = mLocationTemp[0];
796             int top = mLocationTemp[1];
797             mQsMediaHost.getCurrentClipping().set(left, top,
798                     left + getView().getMeasuredWidth(),
799                     top + mQSPanelScrollView.getMeasuredHeight()
800                             - mQSPanelController.getPaddingBottom());
801         }
802     }
803 
updateMediaPositions()804     private void updateMediaPositions() {
805         if (Utils.useQsMediaPlayer(getContext())) {
806             View hostView = mQsMediaHost.getHostView();
807             // Make sure the media appears a bit from the top to make it look nicer
808             if (mLastQSExpansion > 0 && !isKeyguardState() && !mQqsMediaHost.getVisible()
809                     && !mQSPanelController.shouldUseHorizontalLayout() && !mInSplitShade) {
810                 float interpolation = 1.0f - mLastQSExpansion;
811                 interpolation = Interpolators.ACCELERATE.getInterpolation(interpolation);
812                 float translationY = -hostView.getHeight() * 1.3f * interpolation;
813                 hostView.setTranslationY(translationY);
814             } else {
815                 hostView.setTranslationY(0);
816             }
817         }
818     }
819 
headerWillBeAnimating()820     private boolean headerWillBeAnimating() {
821         return mStatusBarState == KEYGUARD && mShowCollapsedOnKeyguard && !isKeyguardState();
822     }
823 
824     @Override
animateHeaderSlidingOut()825     public void animateHeaderSlidingOut() {
826         if (DEBUG) Log.d(TAG, "animateHeaderSlidingOut");
827         if (getView().getY() == -mHeader.getHeight()) {
828             return;
829         }
830         mHeaderAnimating = true;
831         getView().animate().y(-mHeader.getHeight())
832                 .setStartDelay(0)
833                 .setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD)
834                 .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
835                 .setListener(new AnimatorListenerAdapter() {
836                     @Override
837                     public void onAnimationEnd(Animator animation) {
838                         if (getView() != null) {
839                             // The view could be destroyed before the animation completes when
840                             // switching users.
841                             getView().animate().setListener(null);
842                         }
843                         mHeaderAnimating = false;
844                         updateQsState();
845                     }
846                 })
847                 .start();
848     }
849 
850     @Override
setCollapseExpandAction(Runnable action)851     public void setCollapseExpandAction(Runnable action) {
852         mQSPanelController.setCollapseExpandAction(action);
853         mQuickQSPanelController.setCollapseExpandAction(action);
854     }
855 
856     @Override
closeDetail()857     public void closeDetail() {
858         mQSPanelController.closeDetail();
859     }
860 
861     @Override
closeCustomizer()862     public void closeCustomizer() {
863         mQSCustomizerController.hide();
864     }
865 
closeCustomizerImmediately()866     public void closeCustomizerImmediately() {
867         mQSCustomizerController.hide(false);
868     }
869 
notifyCustomizeChanged()870     public void notifyCustomizeChanged() {
871         // The customize state changed, so our height changed.
872         mContainer.updateExpansion();
873         boolean customizing = isCustomizing();
874         if (SceneContainerFlag.isEnabled()) {
875             mQSPanelController.setVisibility(!customizing ? View.VISIBLE : View.INVISIBLE);
876         } else {
877             mQSPanelScrollView.setVisibility(!customizing ? View.VISIBLE : View.INVISIBLE);
878         }
879         mFooter.setVisibility(!customizing ? View.VISIBLE : View.INVISIBLE);
880         if (mFooterActionsView != null) {
881             mFooterActionsView.setVisibility(!customizing ? View.VISIBLE : View.INVISIBLE);
882         }
883         mHeader.setVisibility(!customizing ? View.VISIBLE : View.INVISIBLE);
884         // Let the panel know the position changed and it needs to update where notifications
885         // and whatnot are.
886         if (mPanelView != null) {
887             mPanelView.onQsHeightChanged();
888         }
889     }
890 
891     /**
892      * The height this view wants to be. This is different from {@link View#getMeasuredHeight} such
893      * that during closing the detail panel, this already returns the smaller height.
894      */
895     @Override
getDesiredHeight()896     public int getDesiredHeight() {
897         if (mQSCustomizerController.isCustomizing()) {
898             return getView().getHeight();
899         }
900         return getView().getMeasuredHeight();
901     }
902 
903     @Override
setHeightOverride(int desiredHeight)904     public void setHeightOverride(int desiredHeight) {
905         mContainer.setHeightOverride(desiredHeight);
906     }
907 
908     @Override
getQsMinExpansionHeight()909     public int getQsMinExpansionHeight() {
910         if (mInSplitShade) {
911             return getQsMinExpansionHeightForSplitShade();
912         }
913         return mHeader.getHeight();
914     }
915 
916     /**
917      * Returns the min expansion height for split shade.
918      *
919      * On split shade, QS is always expanded and goes from the top of the screen to the bottom of
920      * the QS container.
921      */
getQsMinExpansionHeightForSplitShade()922     private int getQsMinExpansionHeightForSplitShade() {
923         getView().getLocationOnScreen(mLocationTemp);
924         int top = mLocationTemp[1];
925         // We want to get the original top position, so we subtract any translation currently set.
926         int originalTop = (int) (top - getView().getTranslationY());
927         // On split shade the QS view doesn't start at the top of the screen, so we need to add the
928         // top margin.
929         return originalTop + getView().getHeight();
930     }
931 
932     @Override
hideImmediately()933     public void hideImmediately() {
934         getView().animate().cancel();
935         getView().setY(-getQsMinExpansionHeight());
936     }
937 
938     @Override
onUpcomingStateChanged(int upcomingState)939     public void onUpcomingStateChanged(int upcomingState) {
940         if (upcomingState == KEYGUARD) {
941             // refresh state of QS as soon as possible - while it's still upcoming - so in case of
942             // transition to KEYGUARD (e.g. from unlocked to AOD) all objects are aware they should
943             // already behave like on keyguard. Otherwise we might be doing extra work,
944             // e.g. QSAnimator making QS visible and then quickly invisible
945             onStateChanged(upcomingState);
946         }
947     }
948 
949     @Override
onStateChanged(int newState)950     public void onStateChanged(int newState) {
951         if (SceneContainerFlag.isEnabled() || newState == mStatusBarState) {
952             return;
953         }
954         mStatusBarState = newState;
955         setKeyguardShowing(newState == KEYGUARD);
956         updateShowCollapsedOnKeyguard();
957     }
958 
959     @VisibleForTesting
getListeningAndVisibilityLifecycleOwner()960     public ListeningAndVisibilityLifecycleOwner getListeningAndVisibilityLifecycleOwner() {
961         return mListeningAndVisibilityLifecycleOwner;
962     }
963 
getQQSHeight()964     public int getQQSHeight() {
965         return mContainer.getQqsHeight();
966     }
967 
getQSHeight()968     public int getQSHeight() {
969         return mContainer.getQsHeight();
970     }
971 
972     /**
973      * Pass the size of the navbar when it's at the bottom of the device so it can be used as
974      * padding
975      * @param padding size of the bottom nav bar in px
976      */
applyBottomNavBarToCustomizerPadding(int padding)977     public void applyBottomNavBarToCustomizerPadding(int padding) {
978         mQSCustomizerController.applyBottomNavBarSizeToRecyclerViewPadding(padding);
979     }
980 
981     @NeverCompile
982     @Override
dump(PrintWriter pw, String[] args)983     public void dump(PrintWriter pw, String[] args) {
984         IndentingPrintWriter indentingPw = new IndentingPrintWriter(pw, /* singleIndent= */ "  ");
985         indentingPw.println("QSImpl:");
986         indentingPw.increaseIndent();
987         indentingPw.println("mQsBounds: " + mQsBounds);
988         indentingPw.println("mQsExpanded: " + mQsExpanded);
989         indentingPw.println("mHeaderAnimating: " + mHeaderAnimating);
990         indentingPw.println("mStackScrollerOverscrolling: " + mStackScrollerOverscrolling);
991         indentingPw.println("mListening: " + mListening);
992         indentingPw.println("mQsVisible: " + mQsVisible);
993         indentingPw.println("mLayoutDirection: " + mLayoutDirection);
994         indentingPw.println("mLastQSExpansion: " + mLastQSExpansion);
995         indentingPw.println("mLastPanelFraction: " + mLastPanelFraction);
996         indentingPw.println("mSquishinessFraction: " + mSquishinessFraction);
997         indentingPw.println("mQsDisabled: " + mQsDisabled);
998         indentingPw.println("mTemp: " + Arrays.toString(mLocationTemp));
999         indentingPw.println("mShowCollapsedOnKeyguard: " + mShowCollapsedOnKeyguard);
1000         indentingPw.println("mLastKeyguardAndExpanded: " + mLastKeyguardAndExpanded);
1001         indentingPw.println("mStatusBarState: " + StatusBarState.toString(mStatusBarState));
1002         indentingPw.println("mTmpLocation: " + Arrays.toString(mTmpLocation));
1003         indentingPw.println("mLastViewHeight: " + mLastViewHeight);
1004         indentingPw.println("mLastHeaderTranslation: " + mLastHeaderTranslation);
1005         indentingPw.println("mInSplitShade: " + mInSplitShade);
1006         indentingPw.println("mTransitioningToFullShade: " + mTransitioningToFullShade);
1007         indentingPw.println("mLockscreenToShadeProgress: " + mLockscreenToShadeProgress);
1008         indentingPw.println("mOverScrolling: " + mOverScrolling);
1009         indentingPw.println("mShouldUpdateMediaSquishiness: " + mShouldUpdateMediaSquishiness);
1010         indentingPw.println("isCustomizing: " + mQSCustomizerController.isCustomizing());
1011         View view = getView();
1012         if (view != null) {
1013             indentingPw.println("top: " + view.getTop());
1014             indentingPw.println("y: " + view.getY());
1015             indentingPw.println("translationY: " + view.getTranslationY());
1016             indentingPw.println("alpha: " + view.getAlpha());
1017             indentingPw.println("height: " + view.getHeight());
1018             indentingPw.println("measuredHeight: " + view.getMeasuredHeight());
1019             indentingPw.println("clipBounds: " + view.getClipBounds());
1020         } else {
1021             indentingPw.println("getView(): null");
1022         }
1023         QuickStatusBarHeader header = mHeader;
1024         if (header != null) {
1025             indentingPw.println("headerHeight: " + header.getHeight());
1026             indentingPw.println("Header visibility: " + visibilityToString(header.getVisibility()));
1027         } else {
1028             indentingPw.println("mHeader: null");
1029         }
1030     }
1031 
visibilityToString(int visibility)1032     private static String visibilityToString(int visibility) {
1033         if (visibility == View.VISIBLE) {
1034             return "VISIBLE";
1035         }
1036         if (visibility == View.INVISIBLE) {
1037             return "INVISIBLE";
1038         }
1039         return "GONE";
1040     }
1041 
1042     @Override
getView()1043     public View getView() {
1044         return mRootView;
1045     }
1046 
1047     @Override
getContext()1048     public Context getContext() {
1049         return mRootView.getContext();
1050     }
1051 
getResources()1052     private Resources getResources() {
1053         return getContext().getResources();
1054     }
1055 
1056     /**
1057      * A {@link LifecycleOwner} whose state is driven by the current state of this fragment:
1058      *
1059      *  - DESTROYED when the fragment is destroyed.
1060      *  - CREATED when mListening == mQsVisible == false.
1061      *  - STARTED when mListening == true && mQsVisible == false.
1062      *  - RESUMED when mListening == true && mQsVisible == true.
1063      */
1064     @VisibleForTesting
1065     class ListeningAndVisibilityLifecycleOwner implements LifecycleOwner {
1066         private final LifecycleRegistry mLifecycleRegistry = new LifecycleRegistry(this);
1067         private boolean mDestroyed = false;
1068 
1069         {
updateState()1070             updateState();
1071         }
1072 
1073         @Override
getLifecycle()1074         public Lifecycle getLifecycle() {
1075             return mLifecycleRegistry;
1076         }
1077 
1078         /**
1079          * Update the state of the associated lifecycle. This should be called whenever
1080          * {@code mListening} or {@code mQsVisible} is changed.
1081          */
updateState()1082         public void updateState() {
1083             if (mDestroyed) {
1084                 mLifecycleRegistry.setCurrentState(Lifecycle.State.DESTROYED);
1085                 return;
1086             }
1087 
1088             if (!mListening) {
1089                 mLifecycleRegistry.setCurrentState(Lifecycle.State.CREATED);
1090                 return;
1091             }
1092 
1093             // mListening && !mQsVisible.
1094             if (!mQsVisible) {
1095                 mLifecycleRegistry.setCurrentState(Lifecycle.State.STARTED);
1096                 return;
1097             }
1098 
1099             // mListening && mQsVisible.
1100             mLifecycleRegistry.setCurrentState(Lifecycle.State.RESUMED);
1101         }
1102 
destroy()1103         public void destroy() {
1104             mDestroyed = true;
1105             updateState();
1106         }
1107     }
1108 }
1109