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