1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
5  * except in compliance with the License. You may obtain a copy of the License at
6  *
7  *      http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software distributed under the
10  * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
11  * KIND, either express or implied. See the License for the specific language governing
12  * permissions and limitations under the License.
13  */
14 
15 package com.android.systemui.qs;
16 
17 import static android.app.StatusBarManager.DISABLE2_QUICK_SETTINGS;
18 
19 import static com.android.systemui.media.dagger.MediaModule.QS_PANEL;
20 import static com.android.systemui.media.dagger.MediaModule.QUICK_QS_PANEL;
21 import static com.android.systemui.statusbar.DisableFlagsLogger.DisableState;
22 
23 import android.animation.Animator;
24 import android.animation.AnimatorListenerAdapter;
25 import android.content.res.Configuration;
26 import android.graphics.Rect;
27 import android.os.Bundle;
28 import android.util.Log;
29 import android.view.ContextThemeWrapper;
30 import android.view.LayoutInflater;
31 import android.view.View;
32 import android.view.View.OnClickListener;
33 import android.view.ViewGroup;
34 import android.view.ViewTreeObserver;
35 import android.widget.FrameLayout.LayoutParams;
36 
37 import androidx.annotation.Nullable;
38 import androidx.annotation.VisibleForTesting;
39 
40 import com.android.systemui.R;
41 import com.android.systemui.animation.Interpolators;
42 import com.android.systemui.animation.ShadeInterpolation;
43 import com.android.systemui.dump.DumpManager;
44 import com.android.systemui.media.MediaHost;
45 import com.android.systemui.plugins.FalsingManager;
46 import com.android.systemui.plugins.qs.QS;
47 import com.android.systemui.plugins.qs.QSContainerController;
48 import com.android.systemui.plugins.statusbar.StatusBarStateController;
49 import com.android.systemui.qs.customize.QSCustomizerController;
50 import com.android.systemui.qs.dagger.QSFragmentComponent;
51 import com.android.systemui.statusbar.CommandQueue;
52 import com.android.systemui.statusbar.StatusBarState;
53 import com.android.systemui.statusbar.notification.stack.StackStateAnimator;
54 import com.android.systemui.statusbar.phone.KeyguardBypassController;
55 import com.android.systemui.statusbar.policy.BrightnessMirrorController;
56 import com.android.systemui.statusbar.policy.RemoteInputQuickSettingsDisabler;
57 import com.android.systemui.util.LifecycleFragment;
58 import com.android.systemui.util.Utils;
59 
60 import java.util.function.Consumer;
61 
62 import javax.inject.Inject;
63 import javax.inject.Named;
64 
65 public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Callbacks,
66         StatusBarStateController.StateListener {
67     private static final String TAG = "QS";
68     private static final boolean DEBUG = false;
69     private static final String EXTRA_EXPANDED = "expanded";
70     private static final String EXTRA_LISTENING = "listening";
71 
72     private final Rect mQsBounds = new Rect();
73     private final StatusBarStateController mStatusBarStateController;
74     private final FalsingManager mFalsingManager;
75     private final KeyguardBypassController mBypassController;
76     private boolean mQsExpanded;
77     private boolean mHeaderAnimating;
78     private boolean mStackScrollerOverscrolling;
79 
80     private long mDelay;
81 
82     private QSAnimator mQSAnimator;
83     private HeightListener mPanelView;
84     private QSSquishinessController mQSSquishinessController;
85     protected QuickStatusBarHeader mHeader;
86     protected NonInterceptingScrollView mQSPanelScrollView;
87     private QSDetail mQSDetail;
88     private boolean mListening;
89     private QSContainerImpl mContainer;
90     private int mLayoutDirection;
91     private QSFooter mFooter;
92     private float mLastQSExpansion = -1;
93     private float mLastPanelFraction;
94     private float mSquishinessFraction = 1;
95     private boolean mQsDisabled;
96 
97     private final RemoteInputQuickSettingsDisabler mRemoteInputQuickSettingsDisabler;
98     private final CommandQueue mCommandQueue;
99     private final QSDetailDisplayer mQsDetailDisplayer;
100     private final MediaHost mQsMediaHost;
101     private final MediaHost mQqsMediaHost;
102     private final QSFragmentComponent.Factory mQsComponentFactory;
103     private final QSFragmentDisableFlagsLogger mQsFragmentDisableFlagsLogger;
104     private final QSTileHost mHost;
105     private boolean mShowCollapsedOnKeyguard;
106     private boolean mLastKeyguardAndExpanded;
107     /**
108      * The last received state from the controller. This should not be used directly to check if
109      * we're on keyguard but use {@link #isKeyguardState()} instead since that is more accurate
110      * during state transitions which often call into us.
111      */
112     private int mState;
113     private QSContainerImplController mQSContainerImplController;
114     private int[] mTmpLocation = new int[2];
115     private int mLastViewHeight;
116     private float mLastHeaderTranslation;
117     private QSPanelController mQSPanelController;
118     private QuickQSPanelController mQuickQSPanelController;
119     private QSCustomizerController mQSCustomizerController;
120     private ScrollListener mScrollListener;
121     /**
122      * When true, QS will translate from outside the screen. It will be clipped with parallax
123      * otherwise.
124      */
125     private boolean mInSplitShade;
126     private boolean mPulseExpanding;
127 
128     /**
129      * Are we currently transitioning from lockscreen to the full shade?
130      */
131     private boolean mTransitioningToFullShade;
132 
133     /**
134      * Whether the next Quick settings
135      */
136     private boolean mAnimateNextQsUpdate;
137 
138     private DumpManager mDumpManager;
139 
140     /**
141      * Progress of pull down from the center of the lock screen.
142      * @see com.android.systemui.statusbar.LockscreenShadeTransitionController
143      */
144     private float mFullShadeProgress;
145 
146     @Inject
QSFragment(RemoteInputQuickSettingsDisabler remoteInputQsDisabler, QSTileHost qsTileHost, StatusBarStateController statusBarStateController, CommandQueue commandQueue, QSDetailDisplayer qsDetailDisplayer, @Named(QS_PANEL) MediaHost qsMediaHost, @Named(QUICK_QS_PANEL) MediaHost qqsMediaHost, KeyguardBypassController keyguardBypassController, QSFragmentComponent.Factory qsComponentFactory, QSFragmentDisableFlagsLogger qsFragmentDisableFlagsLogger, FalsingManager falsingManager, DumpManager dumpManager)147     public QSFragment(RemoteInputQuickSettingsDisabler remoteInputQsDisabler,
148             QSTileHost qsTileHost,
149             StatusBarStateController statusBarStateController, CommandQueue commandQueue,
150             QSDetailDisplayer qsDetailDisplayer, @Named(QS_PANEL) MediaHost qsMediaHost,
151             @Named(QUICK_QS_PANEL) MediaHost qqsMediaHost,
152             KeyguardBypassController keyguardBypassController,
153             QSFragmentComponent.Factory qsComponentFactory,
154             QSFragmentDisableFlagsLogger qsFragmentDisableFlagsLogger,
155             FalsingManager falsingManager, DumpManager dumpManager) {
156         mRemoteInputQuickSettingsDisabler = remoteInputQsDisabler;
157         mCommandQueue = commandQueue;
158         mQsDetailDisplayer = qsDetailDisplayer;
159         mQsMediaHost = qsMediaHost;
160         mQqsMediaHost = qqsMediaHost;
161         mQsComponentFactory = qsComponentFactory;
162         mQsFragmentDisableFlagsLogger = qsFragmentDisableFlagsLogger;
163         commandQueue.observe(getLifecycle(), this);
164         mHost = qsTileHost;
165         mFalsingManager = falsingManager;
166         mBypassController = keyguardBypassController;
167         mStatusBarStateController = statusBarStateController;
168         mDumpManager = dumpManager;
169     }
170 
171     @Override
onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState)172     public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
173             Bundle savedInstanceState) {
174         inflater = inflater.cloneInContext(new ContextThemeWrapper(getContext(),
175                 R.style.Theme_SystemUI_QuickSettings));
176         return inflater.inflate(R.layout.qs_panel, container, false);
177     }
178 
179     @Override
onViewCreated(View view, @Nullable Bundle savedInstanceState)180     public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
181         QSFragmentComponent qsFragmentComponent = mQsComponentFactory.create(this);
182         mQSPanelController = qsFragmentComponent.getQSPanelController();
183         mQuickQSPanelController = qsFragmentComponent.getQuickQSPanelController();
184 
185         mQSPanelController.init();
186         mQuickQSPanelController.init();
187 
188         mQSPanelScrollView = view.findViewById(R.id.expanded_qs_scroll_view);
189         mQSPanelScrollView.addOnLayoutChangeListener(
190                 (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
191                     updateQsBounds();
192                 });
193         mQSPanelScrollView.setOnScrollChangeListener(
194                 (v, scrollX, scrollY, oldScrollX, oldScrollY) -> {
195                     // Lazily update animators whenever the scrolling changes
196                     mQSAnimator.requestAnimatorUpdate();
197                     mHeader.setExpandedScrollAmount(scrollY);
198                     if (mScrollListener != null) {
199                         mScrollListener.onQsPanelScrollChanged(scrollY);
200                     }
201         });
202         mQSDetail = view.findViewById(R.id.qs_detail);
203         mHeader = view.findViewById(R.id.header);
204         mQSPanelController.setHeaderContainer(view.findViewById(R.id.header_text_container));
205         mFooter = qsFragmentComponent.getQSFooter();
206 
207         mQsDetailDisplayer.setQsPanelController(mQSPanelController);
208 
209         mQSContainerImplController = qsFragmentComponent.getQSContainerImplController();
210         mQSContainerImplController.init();
211         mContainer = mQSContainerImplController.getView();
212         mDumpManager.registerDumpable(mContainer.getClass().getName(), mContainer);
213 
214         mQSDetail.setQsPanel(mQSPanelController, mHeader, mFooter, mFalsingManager);
215         mQSAnimator = qsFragmentComponent.getQSAnimator();
216         mQSSquishinessController = qsFragmentComponent.getQSSquishinessController();
217 
218         mQSCustomizerController = qsFragmentComponent.getQSCustomizerController();
219         mQSCustomizerController.init();
220         mQSCustomizerController.setQs(this);
221         if (savedInstanceState != null) {
222             setExpanded(savedInstanceState.getBoolean(EXTRA_EXPANDED));
223             setListening(savedInstanceState.getBoolean(EXTRA_LISTENING));
224             setEditLocation(view);
225             mQSCustomizerController.restoreInstanceState(savedInstanceState);
226             if (mQsExpanded) {
227                 mQSPanelController.getTileLayout().restoreInstanceState(savedInstanceState);
228             }
229         }
230         setHost(mHost);
231         mStatusBarStateController.addCallback(this);
232         onStateChanged(mStatusBarStateController.getState());
233         view.addOnLayoutChangeListener(
234                 (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
235                     boolean sizeChanged = (oldTop - oldBottom) != (top - bottom);
236                     if (sizeChanged) {
237                         setQsExpansion(mLastQSExpansion, mLastPanelFraction,
238                                 mLastHeaderTranslation, mSquishinessFraction);
239                     }
240                 });
241         mQSPanelController.setUsingHorizontalLayoutChangeListener(
242                 () -> {
243                     // The hostview may be faded out in the horizontal layout. Let's make sure to
244                     // reset the alpha when switching layouts. This is fine since the animator will
245                     // update the alpha if it's not supposed to be 1.0f
246                     mQSPanelController.getMediaHost().getHostView().setAlpha(1.0f);
247                     mQSAnimator.requestAnimatorUpdate();
248                 });
249     }
250 
251     @Override
setScrollListener(ScrollListener listener)252     public void setScrollListener(ScrollListener listener) {
253         mScrollListener = listener;
254     }
255 
256     @Override
onDestroy()257     public void onDestroy() {
258         super.onDestroy();
259         mStatusBarStateController.removeCallback(this);
260         if (mListening) {
261             setListening(false);
262         }
263         mQSCustomizerController.setQs(null);
264         mQsDetailDisplayer.setQsPanelController(null);
265         mScrollListener = null;
266         mDumpManager.unregisterDumpable(mContainer.getClass().getName());
267     }
268 
269     @Override
onSaveInstanceState(Bundle outState)270     public void onSaveInstanceState(Bundle outState) {
271         super.onSaveInstanceState(outState);
272         outState.putBoolean(EXTRA_EXPANDED, mQsExpanded);
273         outState.putBoolean(EXTRA_LISTENING, mListening);
274         mQSCustomizerController.saveInstanceState(outState);
275         if (mQsExpanded) {
276             mQSPanelController.getTileLayout().saveInstanceState(outState);
277         }
278     }
279 
280     @VisibleForTesting
isListening()281     boolean isListening() {
282         return mListening;
283     }
284 
285     @VisibleForTesting
isExpanded()286     boolean isExpanded() {
287         return mQsExpanded;
288     }
289 
290     @Override
getHeader()291     public View getHeader() {
292         return mHeader;
293     }
294 
295     @Override
setHasNotifications(boolean hasNotifications)296     public void setHasNotifications(boolean hasNotifications) {
297     }
298 
299     @Override
setPanelView(HeightListener panelView)300     public void setPanelView(HeightListener panelView) {
301         mPanelView = panelView;
302     }
303 
304     @Override
onConfigurationChanged(Configuration newConfig)305     public void onConfigurationChanged(Configuration newConfig) {
306         super.onConfigurationChanged(newConfig);
307         setEditLocation(getView());
308         if (newConfig.getLayoutDirection() != mLayoutDirection) {
309             mLayoutDirection = newConfig.getLayoutDirection();
310             if (mQSAnimator != null) {
311                 mQSAnimator.onRtlChanged();
312             }
313         }
314         updateQsState();
315     }
316 
317     @Override
setFancyClipping(int top, int bottom, int cornerRadius, boolean visible)318     public void setFancyClipping(int top, int bottom, int cornerRadius, boolean visible) {
319         if (getView() instanceof QSContainerImpl) {
320             ((QSContainerImpl) getView()).setFancyClipping(top, bottom, cornerRadius, visible);
321         }
322     }
323 
324     @Override
isFullyCollapsed()325     public boolean isFullyCollapsed() {
326         return mLastQSExpansion == 0.0f || mLastQSExpansion == -1;
327     }
328 
329     @Override
setCollapsedMediaVisibilityChangedListener(Consumer<Boolean> listener)330     public void setCollapsedMediaVisibilityChangedListener(Consumer<Boolean> listener) {
331         mQuickQSPanelController.setMediaVisibilityChangedListener(listener);
332     }
333 
setEditLocation(View view)334     private void setEditLocation(View view) {
335         View edit = view.findViewById(android.R.id.edit);
336         int[] loc = edit.getLocationOnScreen();
337         int x = loc[0] + edit.getWidth() / 2;
338         int y = loc[1] + edit.getHeight() / 2;
339         mQSCustomizerController.setEditLocation(x, y);
340     }
341 
342     @Override
setContainerController(QSContainerController controller)343     public void setContainerController(QSContainerController controller) {
344         mQSCustomizerController.setContainerController(controller);
345         mQSDetail.setContainerController(controller);
346     }
347 
348     @Override
isCustomizing()349     public boolean isCustomizing() {
350         return mQSCustomizerController.isCustomizing();
351     }
352 
setHost(QSTileHost qsh)353     public void setHost(QSTileHost qsh) {
354         mQSDetail.setHost(qsh);
355     }
356 
357     @Override
disable(int displayId, int state1, int state2, boolean animate)358     public void disable(int displayId, int state1, int state2, boolean animate) {
359         if (displayId != getContext().getDisplayId()) {
360             return;
361         }
362         int state2BeforeAdjustment = state2;
363         state2 = mRemoteInputQuickSettingsDisabler.adjustDisableFlags(state2);
364 
365         mQsFragmentDisableFlagsLogger.logDisableFlagChange(
366                 /* new= */ new DisableState(state1, state2BeforeAdjustment),
367                 /* newAfterLocalModification= */ new DisableState(state1, state2)
368         );
369 
370         final boolean disabled = (state2 & DISABLE2_QUICK_SETTINGS) != 0;
371         if (disabled == mQsDisabled) return;
372         mQsDisabled = disabled;
373         mContainer.disable(state1, state2, animate);
374         mHeader.disable(state1, state2, animate);
375         mFooter.disable(state1, state2, animate);
376         updateQsState();
377     }
378 
updateQsState()379     private void updateQsState() {
380         final boolean expanded = mQsExpanded || mInSplitShade;
381         final boolean expandVisually = expanded || mStackScrollerOverscrolling
382                 || mHeaderAnimating;
383         mQSPanelController.setExpanded(expanded);
384         mQSDetail.setExpanded(expanded);
385         boolean keyguardShowing = isKeyguardState();
386         mHeader.setVisibility((expanded || !keyguardShowing || mHeaderAnimating
387                 || mShowCollapsedOnKeyguard)
388                 ? View.VISIBLE
389                 : View.INVISIBLE);
390         mHeader.setExpanded((keyguardShowing && !mHeaderAnimating && !mShowCollapsedOnKeyguard)
391                 || (expanded && !mStackScrollerOverscrolling), mQuickQSPanelController);
392         mFooter.setVisibility(!mQsDisabled && (expanded || !keyguardShowing || mHeaderAnimating
393                 || mShowCollapsedOnKeyguard)
394                 ? View.VISIBLE
395                 : View.INVISIBLE);
396         mFooter.setExpanded((keyguardShowing && !mHeaderAnimating && !mShowCollapsedOnKeyguard)
397                 || (expanded && !mStackScrollerOverscrolling));
398         mQSPanelController.setVisibility(
399                 !mQsDisabled && expandVisually ? View.VISIBLE : View.INVISIBLE);
400     }
401 
isKeyguardState()402     private boolean isKeyguardState() {
403         // We want the freshest state here since otherwise we'll have some weirdness if earlier
404         // listeners trigger updates
405         return mStatusBarStateController.getState() == StatusBarState.KEYGUARD;
406     }
407 
updateShowCollapsedOnKeyguard()408     private void updateShowCollapsedOnKeyguard() {
409         boolean showCollapsed = mBypassController.getBypassEnabled()
410                 || (mTransitioningToFullShade && !mInSplitShade);
411         if (showCollapsed != mShowCollapsedOnKeyguard) {
412             mShowCollapsedOnKeyguard = showCollapsed;
413             updateQsState();
414             if (mQSAnimator != null) {
415                 mQSAnimator.setShowCollapsedOnKeyguard(showCollapsed);
416             }
417             if (!showCollapsed && isKeyguardState()) {
418                 setQsExpansion(mLastQSExpansion, mLastPanelFraction, 0,
419                         mSquishinessFraction);
420             }
421         }
422     }
423 
getQSPanelController()424     public QSPanelController getQSPanelController() {
425         return mQSPanelController;
426     }
427 
setBrightnessMirrorController( BrightnessMirrorController brightnessMirrorController)428     public void setBrightnessMirrorController(
429             BrightnessMirrorController brightnessMirrorController) {
430         mQSPanelController.setBrightnessMirror(brightnessMirrorController);
431     }
432 
433     @Override
isShowingDetail()434     public boolean isShowingDetail() {
435         return mQSCustomizerController.isCustomizing() || mQSDetail.isShowingDetail();
436     }
437 
438     @Override
setHeaderClickable(boolean clickable)439     public void setHeaderClickable(boolean clickable) {
440         if (DEBUG) Log.d(TAG, "setHeaderClickable " + clickable);
441     }
442 
443     @Override
setExpanded(boolean expanded)444     public void setExpanded(boolean expanded) {
445         if (DEBUG) Log.d(TAG, "setExpanded " + expanded);
446         mQsExpanded = expanded;
447         mQSPanelController.setListening(mListening, mQsExpanded);
448         updateQsState();
449     }
450 
setKeyguardShowing(boolean keyguardShowing)451     private void setKeyguardShowing(boolean keyguardShowing) {
452         if (DEBUG) Log.d(TAG, "setKeyguardShowing " + keyguardShowing);
453         mLastQSExpansion = -1;
454 
455         if (mQSAnimator != null) {
456             mQSAnimator.setOnKeyguard(keyguardShowing);
457         }
458 
459         mFooter.setKeyguardShowing(keyguardShowing);
460         updateQsState();
461     }
462 
463     @Override
setOverscrolling(boolean stackScrollerOverscrolling)464     public void setOverscrolling(boolean stackScrollerOverscrolling) {
465         if (DEBUG) Log.d(TAG, "setOverscrolling " + stackScrollerOverscrolling);
466         mStackScrollerOverscrolling = stackScrollerOverscrolling;
467         updateQsState();
468     }
469 
470     @Override
setListening(boolean listening)471     public void setListening(boolean listening) {
472         if (DEBUG) Log.d(TAG, "setListening " + listening);
473         mListening = listening;
474         mQSContainerImplController.setListening(listening);
475         mFooter.setListening(listening);
476         mQSPanelController.setListening(mListening, mQsExpanded);
477     }
478 
479     @Override
setHeaderListening(boolean listening)480     public void setHeaderListening(boolean listening) {
481         mQSContainerImplController.setListening(listening);
482         mFooter.setListening(listening);
483     }
484 
485     @Override
setInSplitShade(boolean inSplitShade)486     public void setInSplitShade(boolean inSplitShade) {
487         mInSplitShade = inSplitShade;
488         mQSAnimator.setTranslateWhileExpanding(inSplitShade);
489         updateShowCollapsedOnKeyguard();
490         updateQsState();
491     }
492 
493     @Override
setTransitionToFullShadeAmount(float pxAmount, float progress)494     public void setTransitionToFullShadeAmount(float pxAmount, float progress) {
495         boolean isTransitioningToFullShade = pxAmount > 0;
496         if (isTransitioningToFullShade != mTransitioningToFullShade) {
497             mTransitioningToFullShade = isTransitioningToFullShade;
498             updateShowCollapsedOnKeyguard();
499         }
500         mFullShadeProgress = progress;
501         setQsExpansion(mLastQSExpansion, mLastPanelFraction, mLastHeaderTranslation,
502                 isTransitioningToFullShade ? progress : mSquishinessFraction);
503     }
504 
505     @Override
setQsExpansion(float expansion, float panelExpansionFraction, float proposedTranslation, float squishinessFraction)506     public void setQsExpansion(float expansion, float panelExpansionFraction,
507             float proposedTranslation, float squishinessFraction) {
508         float headerTranslation = mTransitioningToFullShade ? 0 : proposedTranslation;
509         float progress = mTransitioningToFullShade || mState == StatusBarState.KEYGUARD
510                 ? mFullShadeProgress : panelExpansionFraction;
511         setAlphaAnimationProgress(mInSplitShade ? progress : 1);
512         mContainer.setExpansion(expansion);
513         final float translationScaleY = (mInSplitShade
514                 ? 1 : QSAnimator.SHORT_PARALLAX_AMOUNT) * (expansion - 1);
515         boolean onKeyguardAndExpanded = isKeyguardState() && !mShowCollapsedOnKeyguard;
516         if (!mHeaderAnimating && !headerWillBeAnimating()) {
517             getView().setTranslationY(
518                     onKeyguardAndExpanded
519                             ? translationScaleY * mHeader.getHeight()
520                             : headerTranslation);
521         }
522         int currentHeight = getView().getHeight();
523         if (expansion == mLastQSExpansion
524                 && mLastKeyguardAndExpanded == onKeyguardAndExpanded
525                 && mLastViewHeight == currentHeight
526                 && mLastHeaderTranslation == headerTranslation
527                 && mSquishinessFraction == squishinessFraction) {
528             return;
529         }
530         mLastHeaderTranslation = headerTranslation;
531         mLastPanelFraction = panelExpansionFraction;
532         mSquishinessFraction = squishinessFraction;
533         mLastQSExpansion = expansion;
534         mLastKeyguardAndExpanded = onKeyguardAndExpanded;
535         mLastViewHeight = currentHeight;
536 
537         boolean fullyExpanded = expansion == 1;
538         boolean fullyCollapsed = expansion == 0.0f;
539         int heightDiff = mQSPanelScrollView.getBottom() - mHeader.getBottom()
540                 + mHeader.getPaddingBottom();
541         float panelTranslationY = translationScaleY * heightDiff;
542 
543         // Let the views animate their contents correctly by giving them the necessary context.
544         mHeader.setExpansion(onKeyguardAndExpanded, expansion, panelTranslationY);
545         if (expansion < 1 && expansion > 0.99) {
546             if (mQuickQSPanelController.switchTileLayout(false)) {
547                 mHeader.updateResources();
548             }
549         }
550         mFooter.setExpansion(onKeyguardAndExpanded ? 1 : expansion);
551         mQSPanelController.setRevealExpansion(expansion);
552         mQSPanelController.getTileLayout().setExpansion(expansion, proposedTranslation);
553         mQuickQSPanelController.getTileLayout().setExpansion(expansion, proposedTranslation);
554         mQSPanelScrollView.setTranslationY(translationScaleY * heightDiff);
555         if (fullyCollapsed) {
556             mQSPanelScrollView.setScrollY(0);
557         }
558         mQSDetail.setFullyExpanded(fullyExpanded);
559 
560         if (!fullyExpanded) {
561             // Set bounds on the QS panel so it doesn't run over the header when animating.
562             mQsBounds.top = (int) -mQSPanelScrollView.getTranslationY();
563             mQsBounds.right = mQSPanelScrollView.getWidth();
564             mQsBounds.bottom = mQSPanelScrollView.getHeight();
565         }
566         updateQsBounds();
567 
568         if (mQSSquishinessController != null) {
569             mQSSquishinessController.setSquishiness(mSquishinessFraction);
570         }
571         if (mQSAnimator != null) {
572             mQSAnimator.setPosition(expansion);
573         }
574         updateMediaPositions();
575     }
576 
setAlphaAnimationProgress(float progress)577     private void setAlphaAnimationProgress(float progress) {
578         final View view = getView();
579         if (progress == 0 && view.getVisibility() != View.INVISIBLE) {
580             view.setVisibility(View.INVISIBLE);
581         } else if (progress > 0 && view.getVisibility() != View.VISIBLE) {
582             view.setVisibility((View.VISIBLE));
583         }
584         float alpha = ShadeInterpolation.getContentAlpha(progress);
585         view.setAlpha(alpha);
586     }
587 
updateQsBounds()588     private void updateQsBounds() {
589         if (mLastQSExpansion == 1.0f) {
590             // Fully expanded, let's set the layout bounds as clip bounds. This is necessary because
591             // it's a scrollview and otherwise wouldn't be clipped. However, we set the horizontal
592             // bounds so the pages go to the ends of QSContainerImpl
593             ViewGroup.MarginLayoutParams lp =
594                     (ViewGroup.MarginLayoutParams) mQSPanelScrollView.getLayoutParams();
595             mQsBounds.set(-lp.leftMargin, 0, mQSPanelScrollView.getWidth() + lp.rightMargin,
596                     mQSPanelScrollView.getHeight());
597         }
598         mQSPanelScrollView.setClipBounds(mQsBounds);
599     }
600 
updateMediaPositions()601     private void updateMediaPositions() {
602         if (Utils.useQsMediaPlayer(getContext())) {
603             mContainer.getLocationOnScreen(mTmpLocation);
604             float absoluteBottomPosition = mTmpLocation[1] + mContainer.getHeight();
605             // The Media can be scrolled off screen by default, let's offset it
606             float expandedMediaPosition = absoluteBottomPosition - mQSPanelScrollView.getScrollY()
607                     + mQSPanelScrollView.getScrollRange();
608             pinToBottom(expandedMediaPosition, mQsMediaHost, true /* expanded */);
609             // The expanded media host should never move above the laid out position
610             pinToBottom(absoluteBottomPosition, mQqsMediaHost, false /* expanded */);
611         }
612     }
613 
pinToBottom(float absoluteBottomPosition, MediaHost mediaHost, boolean expanded)614     private void pinToBottom(float absoluteBottomPosition, MediaHost mediaHost, boolean expanded) {
615         View hostView = mediaHost.getHostView();
616         // On keyguard we cross-fade to expanded, so no need to pin it.
617         // If the collapsed qs isn't visible, we also just keep it at the laid out position.
618         if (mLastQSExpansion > 0 && !isKeyguardState() && mQqsMediaHost.getVisible()) {
619             float targetPosition = absoluteBottomPosition - getTotalBottomMargin(hostView)
620                     - hostView.getHeight();
621             float currentPosition = mediaHost.getCurrentBounds().top
622                     - hostView.getTranslationY();
623             float translationY = targetPosition - currentPosition;
624             if (expanded) {
625                 // Never go below the laid out position. This is necessary since the qs panel can
626                 // change in height and we don't want to ever go below it's position
627                 translationY = Math.min(translationY, 0);
628             } else {
629                 translationY = Math.max(translationY, 0);
630             }
631             hostView.setTranslationY(translationY);
632         } else {
633             hostView.setTranslationY(0);
634         }
635     }
636 
getTotalBottomMargin(View startView)637     private float getTotalBottomMargin(View startView) {
638         int result = 0;
639         View child = startView;
640         View parent = (View) startView.getParent();
641         while (!(parent instanceof QSContainerImpl) && parent != null) {
642             result += parent.getHeight() - child.getBottom();
643             child = parent;
644             parent = (View) parent.getParent();
645         }
646         return result;
647     }
648 
headerWillBeAnimating()649     private boolean headerWillBeAnimating() {
650         return mState == StatusBarState.KEYGUARD && mShowCollapsedOnKeyguard
651                 && !isKeyguardState();
652     }
653 
654     @Override
animateHeaderSlidingOut()655     public void animateHeaderSlidingOut() {
656         if (DEBUG) Log.d(TAG, "animateHeaderSlidingOut");
657         if (getView().getY() == -mHeader.getHeight()) {
658             return;
659         }
660         mHeaderAnimating = true;
661         getView().animate().y(-mHeader.getHeight())
662                 .setStartDelay(0)
663                 .setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD)
664                 .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
665                 .setListener(new AnimatorListenerAdapter() {
666                     @Override
667                     public void onAnimationEnd(Animator animation) {
668                         if (getView() != null) {
669                             // The view could be destroyed before the animation completes when
670                             // switching users.
671                             getView().animate().setListener(null);
672                         }
673                         mHeaderAnimating = false;
674                         updateQsState();
675                     }
676                 })
677                 .start();
678     }
679 
680     @Override
setExpandClickListener(OnClickListener onClickListener)681     public void setExpandClickListener(OnClickListener onClickListener) {
682         mFooter.setExpandClickListener(onClickListener);
683     }
684 
685     @Override
closeDetail()686     public void closeDetail() {
687         mQSPanelController.closeDetail();
688     }
689 
690     @Override
closeCustomizer()691     public void closeCustomizer() {
692         mQSCustomizerController.hide();
693     }
694 
notifyCustomizeChanged()695     public void notifyCustomizeChanged() {
696         // The customize state changed, so our height changed.
697         mContainer.updateExpansion();
698         boolean customizing = isCustomizing();
699         mQSPanelScrollView.setVisibility(!customizing ? View.VISIBLE : View.INVISIBLE);
700         mFooter.setVisibility(!customizing ? View.VISIBLE : View.INVISIBLE);
701         mHeader.setVisibility(!customizing ? View.VISIBLE : View.INVISIBLE);
702         // Let the panel know the position changed and it needs to update where notifications
703         // and whatnot are.
704         mPanelView.onQsHeightChanged();
705     }
706 
707     /**
708      * The height this view wants to be. This is different from {@link View#getMeasuredHeight} such
709      * that during closing the detail panel, this already returns the smaller height.
710      */
711     @Override
getDesiredHeight()712     public int getDesiredHeight() {
713         if (mQSCustomizerController.isCustomizing()) {
714             return getView().getHeight();
715         }
716         if (mQSDetail.isClosingDetail()) {
717             LayoutParams layoutParams = (LayoutParams) mQSPanelScrollView.getLayoutParams();
718             int panelHeight = layoutParams.topMargin + layoutParams.bottomMargin +
719                     + mQSPanelScrollView.getMeasuredHeight();
720             return panelHeight + getView().getPaddingBottom();
721         } else {
722             return getView().getMeasuredHeight();
723         }
724     }
725 
726     @Override
setHeightOverride(int desiredHeight)727     public void setHeightOverride(int desiredHeight) {
728         mContainer.setHeightOverride(desiredHeight);
729     }
730 
731     @Override
getQsMinExpansionHeight()732     public int getQsMinExpansionHeight() {
733         return mHeader.getHeight();
734     }
735 
736     @Override
hideImmediately()737     public void hideImmediately() {
738         getView().animate().cancel();
739         getView().setY(-mHeader.getHeight());
740     }
741 
742     private final ViewTreeObserver.OnPreDrawListener mStartHeaderSlidingIn
743             = new ViewTreeObserver.OnPreDrawListener() {
744         @Override
745         public boolean onPreDraw() {
746             getView().getViewTreeObserver().removeOnPreDrawListener(this);
747             getView().animate()
748                     .translationY(0f)
749                     .setStartDelay(mDelay)
750                     .setDuration(StackStateAnimator.ANIMATION_DURATION_GO_TO_FULL_SHADE)
751                     .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
752                     .setListener(mAnimateHeaderSlidingInListener)
753                     .start();
754             return true;
755         }
756     };
757 
758     private final Animator.AnimatorListener mAnimateHeaderSlidingInListener
759             = new AnimatorListenerAdapter() {
760         @Override
761         public void onAnimationEnd(Animator animation) {
762             mHeaderAnimating = false;
763             updateQsState();
764             // Unset the listener, otherwise this may persist for another view property animation
765             getView().animate().setListener(null);
766         }
767     };
768 
769     @Override
onStateChanged(int newState)770     public void onStateChanged(int newState) {
771         mState = newState;
772         setKeyguardShowing(newState == StatusBarState.KEYGUARD);
773         updateShowCollapsedOnKeyguard();
774     }
775 }
776