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