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