| /* |
| * Copyright (C) 2016 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file |
| * except in compliance with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software distributed under the |
| * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the specific language governing |
| * permissions and limitations under the License. |
| */ |
| |
| package com.android.systemui.qs; |
| |
| import static android.app.StatusBarManager.DISABLE2_QUICK_SETTINGS; |
| |
| import static com.android.systemui.media.dagger.MediaModule.QS_PANEL; |
| import static com.android.systemui.media.dagger.MediaModule.QUICK_QS_PANEL; |
| import static com.android.systemui.statusbar.StatusBarState.KEYGUARD; |
| import static com.android.systemui.statusbar.StatusBarState.SHADE_LOCKED; |
| import static com.android.systemui.statusbar.disableflags.DisableFlagsLogger.DisableState; |
| |
| import android.animation.Animator; |
| import android.animation.AnimatorListenerAdapter; |
| import android.content.res.Configuration; |
| import android.graphics.Rect; |
| import android.os.Bundle; |
| import android.os.Trace; |
| import android.util.IndentingPrintWriter; |
| import android.util.Log; |
| import android.view.ContextThemeWrapper; |
| import android.view.LayoutInflater; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.view.ViewTreeObserver; |
| import android.widget.LinearLayout; |
| |
| import androidx.annotation.FloatRange; |
| import androidx.annotation.Nullable; |
| import androidx.annotation.VisibleForTesting; |
| import androidx.lifecycle.Lifecycle; |
| import androidx.lifecycle.LifecycleOwner; |
| import androidx.lifecycle.LifecycleRegistry; |
| |
| import com.android.app.animation.Interpolators; |
| import com.android.keyguard.BouncerPanelExpansionCalculator; |
| import com.android.systemui.Dumpable; |
| import com.android.systemui.R; |
| import com.android.systemui.animation.ShadeInterpolation; |
| import com.android.systemui.compose.ComposeFacade; |
| import com.android.systemui.dump.DumpManager; |
| import com.android.systemui.flags.FeatureFlags; |
| import com.android.systemui.flags.Flags; |
| import com.android.systemui.media.controls.ui.MediaHost; |
| import com.android.systemui.plugins.qs.QS; |
| import com.android.systemui.plugins.qs.QSContainerController; |
| import com.android.systemui.plugins.statusbar.StatusBarStateController; |
| import com.android.systemui.qs.customize.QSCustomizerController; |
| import com.android.systemui.qs.dagger.QSFragmentComponent; |
| import com.android.systemui.qs.footer.ui.binder.FooterActionsViewBinder; |
| import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel; |
| import com.android.systemui.qs.logging.QSLogger; |
| import com.android.systemui.shade.transition.LargeScreenShadeInterpolator; |
| import com.android.systemui.statusbar.CommandQueue; |
| import com.android.systemui.statusbar.StatusBarState; |
| import com.android.systemui.statusbar.SysuiStatusBarStateController; |
| import com.android.systemui.statusbar.notification.stack.StackStateAnimator; |
| import com.android.systemui.statusbar.phone.KeyguardBypassController; |
| import com.android.systemui.statusbar.policy.BrightnessMirrorController; |
| import com.android.systemui.statusbar.policy.RemoteInputQuickSettingsDisabler; |
| import com.android.systemui.util.LifecycleFragment; |
| import com.android.systemui.util.Utils; |
| |
| import java.io.PrintWriter; |
| import java.util.Arrays; |
| import java.util.function.Consumer; |
| |
| import javax.inject.Inject; |
| import javax.inject.Named; |
| |
| public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Callbacks, |
| StatusBarStateController.StateListener, Dumpable { |
| private static final String TAG = "QS"; |
| private static final boolean DEBUG = false; |
| private static final String EXTRA_EXPANDED = "expanded"; |
| private static final String EXTRA_LISTENING = "listening"; |
| private static final String EXTRA_VISIBLE = "visible"; |
| |
| private final Rect mQsBounds = new Rect(); |
| private final SysuiStatusBarStateController mStatusBarStateController; |
| private final KeyguardBypassController mBypassController; |
| private boolean mQsExpanded; |
| private boolean mHeaderAnimating; |
| private boolean mStackScrollerOverscrolling; |
| |
| private QSAnimator mQSAnimator; |
| private HeightListener mPanelView; |
| private QSSquishinessController mQSSquishinessController; |
| protected QuickStatusBarHeader mHeader; |
| protected NonInterceptingScrollView mQSPanelScrollView; |
| private boolean mListening; |
| private QSContainerImpl mContainer; |
| private int mLayoutDirection; |
| private QSFooter mFooter; |
| private float mLastQSExpansion = -1; |
| private float mLastPanelFraction; |
| private float mSquishinessFraction = 1; |
| private boolean mQsDisabled; |
| private int[] mLocationTemp = new int[2]; |
| |
| private final RemoteInputQuickSettingsDisabler mRemoteInputQuickSettingsDisabler; |
| private final MediaHost mQsMediaHost; |
| private final MediaHost mQqsMediaHost; |
| private final QSFragmentComponent.Factory mQsComponentFactory; |
| private final QSFragmentDisableFlagsLogger mQsFragmentDisableFlagsLogger; |
| private final LargeScreenShadeInterpolator mLargeScreenShadeInterpolator; |
| private final FeatureFlags mFeatureFlags; |
| private final QSLogger mLogger; |
| private final FooterActionsController mFooterActionsController; |
| private final FooterActionsViewModel.Factory mFooterActionsViewModelFactory; |
| private final FooterActionsViewBinder mFooterActionsViewBinder; |
| private final ListeningAndVisibilityLifecycleOwner mListeningAndVisibilityLifecycleOwner; |
| private boolean mShowCollapsedOnKeyguard; |
| private boolean mLastKeyguardAndExpanded; |
| /** |
| * The last received state from the controller. This should not be used directly to check if |
| * we're on keyguard but use {@link #isKeyguardState()} instead since that is more accurate |
| * during state transitions which often call into us. |
| */ |
| private int mStatusBarState = -1; |
| private QSContainerImplController mQSContainerImplController; |
| private int[] mTmpLocation = new int[2]; |
| private int mLastViewHeight; |
| private float mLastHeaderTranslation; |
| private QSPanelController mQSPanelController; |
| private QuickQSPanelController mQuickQSPanelController; |
| private QSCustomizerController mQSCustomizerController; |
| private FooterActionsViewModel mQSFooterActionsViewModel; |
| @Nullable |
| private ScrollListener mScrollListener; |
| /** |
| * When true, QS will translate from outside the screen. It will be clipped with parallax |
| * otherwise. |
| */ |
| private boolean mInSplitShade; |
| |
| /** |
| * Are we currently transitioning from lockscreen to the full shade? |
| */ |
| private boolean mTransitioningToFullShade; |
| |
| private final DumpManager mDumpManager; |
| |
| /** |
| * Progress of pull down from the center of the lock screen. |
| * @see com.android.systemui.statusbar.LockscreenShadeTransitionController |
| */ |
| private float mLockscreenToShadeProgress; |
| |
| private boolean mOverScrolling; |
| |
| // Whether QQS or QS is visible. When in lockscreen, this is true if and only if QQS or QS is |
| // visible; |
| private boolean mQsVisible; |
| |
| private boolean mIsSmallScreen; |
| |
| @Inject |
| public 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) { |
| mRemoteInputQuickSettingsDisabler = remoteInputQsDisabler; |
| mQsMediaHost = qsMediaHost; |
| mQqsMediaHost = qqsMediaHost; |
| mQsComponentFactory = qsComponentFactory; |
| mQsFragmentDisableFlagsLogger = qsFragmentDisableFlagsLogger; |
| mLogger = qsLogger; |
| mLargeScreenShadeInterpolator = largeScreenShadeInterpolator; |
| mFeatureFlags = featureFlags; |
| commandQueue.observe(getLifecycle(), this); |
| mBypassController = keyguardBypassController; |
| mStatusBarStateController = statusBarStateController; |
| mDumpManager = dumpManager; |
| mFooterActionsController = footerActionsController; |
| mFooterActionsViewModelFactory = footerActionsViewModelFactory; |
| mFooterActionsViewBinder = footerActionsViewBinder; |
| mListeningAndVisibilityLifecycleOwner = new ListeningAndVisibilityLifecycleOwner(); |
| } |
| |
| @Override |
| public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, |
| Bundle savedInstanceState) { |
| try { |
| Trace.beginSection("QSFragment#onCreateView"); |
| inflater = inflater.cloneInContext(new ContextThemeWrapper(getContext(), |
| R.style.Theme_SystemUI_QuickSettings)); |
| return inflater.inflate(R.layout.qs_panel, container, false); |
| } finally { |
| Trace.endSection(); |
| } |
| } |
| |
| @Override |
| public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { |
| QSFragmentComponent qsFragmentComponent = mQsComponentFactory.create(this); |
| mQSPanelController = qsFragmentComponent.getQSPanelController(); |
| mQuickQSPanelController = qsFragmentComponent.getQuickQSPanelController(); |
| |
| mQSPanelController.init(); |
| mQuickQSPanelController.init(); |
| |
| mQSFooterActionsViewModel = mFooterActionsViewModelFactory.create(/* lifecycleOwner */ |
| this); |
| bindFooterActionsView(view); |
| mFooterActionsController.init(); |
| |
| mQSPanelScrollView = view.findViewById(R.id.expanded_qs_scroll_view); |
| mQSPanelScrollView.addOnLayoutChangeListener( |
| (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { |
| updateQsBounds(); |
| }); |
| mQSPanelScrollView.setOnScrollChangeListener( |
| (v, scrollX, scrollY, oldScrollX, oldScrollY) -> { |
| // Lazily update animators whenever the scrolling changes |
| mQSAnimator.requestAnimatorUpdate(); |
| if (mScrollListener != null) { |
| mScrollListener.onQsPanelScrollChanged(scrollY); |
| } |
| }); |
| mHeader = view.findViewById(R.id.header); |
| mFooter = qsFragmentComponent.getQSFooter(); |
| |
| mQSContainerImplController = qsFragmentComponent.getQSContainerImplController(); |
| mQSContainerImplController.init(); |
| mContainer = mQSContainerImplController.getView(); |
| mDumpManager.registerDumpable(mContainer.getClass().getName(), mContainer); |
| |
| mQSAnimator = qsFragmentComponent.getQSAnimator(); |
| mQSSquishinessController = qsFragmentComponent.getQSSquishinessController(); |
| |
| mQSCustomizerController = qsFragmentComponent.getQSCustomizerController(); |
| mQSCustomizerController.init(); |
| mQSCustomizerController.setQs(this); |
| if (savedInstanceState != null) { |
| setQsVisible(savedInstanceState.getBoolean(EXTRA_VISIBLE)); |
| setExpanded(savedInstanceState.getBoolean(EXTRA_EXPANDED)); |
| setListening(savedInstanceState.getBoolean(EXTRA_LISTENING)); |
| setEditLocation(view); |
| mQSCustomizerController.restoreInstanceState(savedInstanceState); |
| if (mQsExpanded) { |
| mQSPanelController.getTileLayout().restoreInstanceState(savedInstanceState); |
| } |
| } |
| mStatusBarStateController.addCallback(this); |
| onStateChanged(mStatusBarStateController.getState()); |
| view.addOnLayoutChangeListener( |
| (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { |
| boolean sizeChanged = (oldTop - oldBottom) != (top - bottom); |
| if (sizeChanged) { |
| setQsExpansion(mLastQSExpansion, mLastPanelFraction, |
| mLastHeaderTranslation, mSquishinessFraction); |
| } |
| }); |
| mQSPanelController.setUsingHorizontalLayoutChangeListener( |
| () -> { |
| // The hostview may be faded out in the horizontal layout. Let's make sure to |
| // reset the alpha when switching layouts. This is fine since the animator will |
| // update the alpha if it's not supposed to be 1.0f |
| mQSPanelController.getMediaHost().getHostView().setAlpha(1.0f); |
| mQSAnimator.requestAnimatorUpdate(); |
| }); |
| } |
| |
| private void bindFooterActionsView(View root) { |
| LinearLayout footerActionsView = root.findViewById(R.id.qs_footer_actions); |
| |
| if (!ComposeFacade.INSTANCE.isComposeAvailable()) { |
| Log.d(TAG, "Binding the View implementation of the QS footer actions"); |
| mFooterActionsViewBinder.bind(footerActionsView, mQSFooterActionsViewModel, |
| mListeningAndVisibilityLifecycleOwner); |
| return; |
| } |
| |
| // Compose is available, so let's use the Compose implementation of the footer actions. |
| Log.d(TAG, "Binding the Compose implementation of the QS footer actions"); |
| View composeView = ComposeFacade.INSTANCE.createFooterActionsView(root.getContext(), |
| mQSFooterActionsViewModel, mListeningAndVisibilityLifecycleOwner); |
| |
| // The id R.id.qs_footer_actions is used by QSContainerImpl to set the horizontal margin |
| // to all views except for qs_footer_actions, so we set it to the Compose view. |
| composeView.setId(R.id.qs_footer_actions); |
| |
| // Replace the View by the Compose provided one. |
| ViewGroup parent = (ViewGroup) footerActionsView.getParent(); |
| ViewGroup.LayoutParams layoutParams = footerActionsView.getLayoutParams(); |
| int index = parent.indexOfChild(footerActionsView); |
| parent.removeViewAt(index); |
| parent.addView(composeView, index, layoutParams); |
| } |
| |
| @Override |
| public void setScrollListener(ScrollListener listener) { |
| mScrollListener = listener; |
| } |
| |
| @Override |
| public void onCreate(Bundle savedInstanceState) { |
| super.onCreate(savedInstanceState); |
| mDumpManager.registerDumpable(getClass().getName(), this); |
| } |
| |
| @Override |
| public void onDestroy() { |
| super.onDestroy(); |
| mStatusBarStateController.removeCallback(this); |
| if (mListening) { |
| setListening(false); |
| } |
| if (mQSCustomizerController != null) { |
| mQSCustomizerController.setQs(null); |
| } |
| mScrollListener = null; |
| if (mContainer != null) { |
| mDumpManager.unregisterDumpable(mContainer.getClass().getName()); |
| } |
| mDumpManager.unregisterDumpable(getClass().getName()); |
| mListeningAndVisibilityLifecycleOwner.destroy(); |
| } |
| |
| @Override |
| public void onSaveInstanceState(Bundle outState) { |
| super.onSaveInstanceState(outState); |
| outState.putBoolean(EXTRA_EXPANDED, mQsExpanded); |
| outState.putBoolean(EXTRA_LISTENING, mListening); |
| outState.putBoolean(EXTRA_VISIBLE, mQsVisible); |
| if (mQSCustomizerController != null) { |
| mQSCustomizerController.saveInstanceState(outState); |
| } |
| if (mQsExpanded) { |
| mQSPanelController.getTileLayout().saveInstanceState(outState); |
| } |
| } |
| |
| @VisibleForTesting |
| boolean isListening() { |
| return mListening; |
| } |
| |
| @VisibleForTesting |
| boolean isExpanded() { |
| return mQsExpanded; |
| } |
| |
| @VisibleForTesting |
| boolean isQsVisible() { |
| return mQsVisible; |
| } |
| |
| @Override |
| public View getHeader() { |
| return mHeader; |
| } |
| |
| @Override |
| public void setHasNotifications(boolean hasNotifications) { |
| } |
| |
| @Override |
| public void setPanelView(HeightListener panelView) { |
| mPanelView = panelView; |
| } |
| |
| @Override |
| public void onConfigurationChanged(Configuration newConfig) { |
| super.onConfigurationChanged(newConfig); |
| setEditLocation(getView()); |
| if (newConfig.getLayoutDirection() != mLayoutDirection) { |
| mLayoutDirection = newConfig.getLayoutDirection(); |
| if (mQSAnimator != null) { |
| mQSAnimator.onRtlChanged(); |
| } |
| } |
| updateQsState(); |
| } |
| |
| @Override |
| public void setFancyClipping(int leftInset, int top, int rightInset, int bottom, |
| int cornerRadius, boolean visible, boolean fullWidth) { |
| if (getView() instanceof QSContainerImpl) { |
| ((QSContainerImpl) getView()).setFancyClipping(leftInset, top, rightInset, bottom, |
| cornerRadius, visible, fullWidth); |
| } |
| } |
| |
| @Override |
| public boolean isFullyCollapsed() { |
| return mLastQSExpansion == 0.0f || mLastQSExpansion == -1; |
| } |
| |
| @Override |
| public void setCollapsedMediaVisibilityChangedListener(Consumer<Boolean> listener) { |
| mQuickQSPanelController.setMediaVisibilityChangedListener(listener); |
| } |
| |
| private void setEditLocation(View view) { |
| View edit = view.findViewById(android.R.id.edit); |
| int[] loc = edit.getLocationOnScreen(); |
| int x = loc[0] + edit.getWidth() / 2; |
| int y = loc[1] + edit.getHeight() / 2; |
| mQSCustomizerController.setEditLocation(x, y); |
| } |
| |
| @Override |
| public void setContainerController(QSContainerController controller) { |
| mQSCustomizerController.setContainerController(controller); |
| } |
| |
| @Override |
| public boolean isCustomizing() { |
| return mQSCustomizerController.isCustomizing(); |
| } |
| |
| @Override |
| public void disable(int displayId, int state1, int state2, boolean animate) { |
| if (displayId != getContext().getDisplayId()) { |
| return; |
| } |
| int state2BeforeAdjustment = state2; |
| state2 = mRemoteInputQuickSettingsDisabler.adjustDisableFlags(state2); |
| |
| mQsFragmentDisableFlagsLogger.logDisableFlagChange( |
| /* new= */ new DisableState(state1, state2BeforeAdjustment), |
| /* newAfterLocalModification= */ new DisableState(state1, state2) |
| ); |
| |
| final boolean disabled = (state2 & DISABLE2_QUICK_SETTINGS) != 0; |
| if (disabled == mQsDisabled) return; |
| mQsDisabled = disabled; |
| mContainer.disable(state1, state2, animate); |
| mHeader.disable(state1, state2, animate); |
| mFooter.disable(state1, state2, animate); |
| updateQsState(); |
| } |
| |
| private void updateQsState() { |
| final boolean expandVisually = mQsExpanded || mStackScrollerOverscrolling |
| || mHeaderAnimating; |
| mQSPanelController.setExpanded(mQsExpanded); |
| boolean keyguardShowing = isKeyguardState(); |
| mHeader.setVisibility((mQsExpanded || !keyguardShowing || mHeaderAnimating |
| || mShowCollapsedOnKeyguard) |
| ? View.VISIBLE |
| : View.INVISIBLE); |
| mHeader.setExpanded((keyguardShowing && !mHeaderAnimating && !mShowCollapsedOnKeyguard) |
| || (mQsExpanded && !mStackScrollerOverscrolling), mQuickQSPanelController); |
| boolean qsPanelVisible = !mQsDisabled && expandVisually; |
| boolean footerVisible = qsPanelVisible && (mQsExpanded || !keyguardShowing |
| || mHeaderAnimating || mShowCollapsedOnKeyguard); |
| mFooter.setVisibility(footerVisible ? View.VISIBLE : View.INVISIBLE); |
| mQSFooterActionsViewModel.onVisibilityChangeRequested(footerVisible); |
| mFooter.setExpanded((keyguardShowing && !mHeaderAnimating && !mShowCollapsedOnKeyguard) |
| || (mQsExpanded && !mStackScrollerOverscrolling)); |
| mQSPanelController.setVisibility(qsPanelVisible ? View.VISIBLE : View.INVISIBLE); |
| if (DEBUG) { |
| Log.d(TAG, "Footer: " + footerVisible + ", QS Panel: " + qsPanelVisible); |
| } |
| } |
| |
| private boolean isKeyguardState() { |
| // We want the freshest state here since otherwise we'll have some weirdness if earlier |
| // listeners trigger updates |
| return mStatusBarStateController.getCurrentOrUpcomingState() == KEYGUARD; |
| } |
| |
| private void updateShowCollapsedOnKeyguard() { |
| boolean showCollapsed = mBypassController.getBypassEnabled() |
| || (mTransitioningToFullShade && !mInSplitShade); |
| if (showCollapsed != mShowCollapsedOnKeyguard) { |
| mShowCollapsedOnKeyguard = showCollapsed; |
| updateQsState(); |
| if (mQSAnimator != null) { |
| mQSAnimator.setShowCollapsedOnKeyguard(showCollapsed); |
| } |
| if (!showCollapsed && isKeyguardState()) { |
| setQsExpansion(mLastQSExpansion, mLastPanelFraction, 0, |
| mSquishinessFraction); |
| } |
| } |
| } |
| |
| public QSPanelController getQSPanelController() { |
| return mQSPanelController; |
| } |
| |
| public void setBrightnessMirrorController( |
| BrightnessMirrorController brightnessMirrorController) { |
| mQSPanelController.setBrightnessMirror(brightnessMirrorController); |
| } |
| |
| @Override |
| public boolean isShowingDetail() { |
| return mQSCustomizerController.isCustomizing(); |
| } |
| |
| @Override |
| public void setHeaderClickable(boolean clickable) { |
| if (DEBUG) Log.d(TAG, "setHeaderClickable " + clickable); |
| } |
| |
| @Override |
| public void setExpanded(boolean expanded) { |
| if (DEBUG) Log.d(TAG, "setExpanded " + expanded); |
| mQsExpanded = expanded; |
| if (mInSplitShade && mQsExpanded) { |
| // in split shade QS is expanded immediately when shade expansion starts and then we |
| // also need to listen to changes - otherwise QS is updated only once its fully expanded |
| setListening(true); |
| } else { |
| updateQsPanelControllerListening(); |
| } |
| updateQsState(); |
| } |
| |
| private void setKeyguardShowing(boolean keyguardShowing) { |
| if (DEBUG) Log.d(TAG, "setKeyguardShowing " + keyguardShowing); |
| mLastQSExpansion = -1; |
| |
| if (mQSAnimator != null) { |
| mQSAnimator.setOnKeyguard(keyguardShowing); |
| } |
| |
| mFooter.setKeyguardShowing(keyguardShowing); |
| updateQsState(); |
| } |
| |
| @Override |
| public void setOverscrolling(boolean stackScrollerOverscrolling) { |
| if (DEBUG) Log.d(TAG, "setOverscrolling " + stackScrollerOverscrolling); |
| mStackScrollerOverscrolling = stackScrollerOverscrolling; |
| updateQsState(); |
| } |
| |
| @Override |
| public void setListening(boolean listening) { |
| if (DEBUG) Log.d(TAG, "setListening " + listening); |
| mListening = listening; |
| mQSContainerImplController.setListening(listening && mQsVisible); |
| mListeningAndVisibilityLifecycleOwner.updateState(); |
| updateQsPanelControllerListening(); |
| } |
| |
| private void updateQsPanelControllerListening() { |
| mQSPanelController.setListening(mListening && mQsVisible, mQsExpanded); |
| } |
| |
| @Override |
| public void setQsVisible(boolean visible) { |
| if (DEBUG) Log.d(TAG, "setQsVisible " + visible); |
| mQsVisible = visible; |
| setListening(mListening); |
| mListeningAndVisibilityLifecycleOwner.updateState(); |
| } |
| |
| @Override |
| public void setHeaderListening(boolean listening) { |
| mQSContainerImplController.setListening(listening); |
| } |
| |
| @Override |
| public void setInSplitShade(boolean inSplitShade) { |
| mInSplitShade = inSplitShade; |
| updateShowCollapsedOnKeyguard(); |
| updateQsState(); |
| } |
| |
| @Override |
| public void setTransitionToFullShadeProgress( |
| boolean isTransitioningToFullShade, |
| @FloatRange(from = 0.0, to = 1.0) float qsTransitionFraction, |
| @FloatRange(from = 0.0, to = 1.0) float qsSquishinessFraction) { |
| if (isTransitioningToFullShade != mTransitioningToFullShade) { |
| mTransitioningToFullShade = isTransitioningToFullShade; |
| updateShowCollapsedOnKeyguard(); |
| } |
| mLockscreenToShadeProgress = qsTransitionFraction; |
| setQsExpansion(mLastQSExpansion, mLastPanelFraction, mLastHeaderTranslation, |
| isTransitioningToFullShade ? qsSquishinessFraction : mSquishinessFraction); |
| } |
| |
| @Override |
| public void setOverScrollAmount(int overScrollAmount) { |
| mOverScrolling = overScrollAmount != 0; |
| View view = getView(); |
| if (view != null) { |
| view.setTranslationY(overScrollAmount); |
| } |
| } |
| |
| @Override |
| public int getHeightDiff() { |
| return mQSPanelScrollView.getBottom() - mHeader.getBottom() |
| + mHeader.getPaddingBottom(); |
| } |
| |
| @Override |
| public void setIsNotificationPanelFullWidth(boolean isFullWidth) { |
| mIsSmallScreen = isFullWidth; |
| } |
| |
| @Override |
| public void setQsExpansion(float expansion, float panelExpansionFraction, |
| float proposedTranslation, float squishinessFraction) { |
| float headerTranslation = mTransitioningToFullShade ? 0 : proposedTranslation; |
| float alphaProgress = calculateAlphaProgress(panelExpansionFraction); |
| setAlphaAnimationProgress(alphaProgress); |
| mContainer.setExpansion(expansion); |
| final float translationScaleY = (mInSplitShade |
| ? 1 : QSAnimator.SHORT_PARALLAX_AMOUNT) * (expansion - 1); |
| boolean onKeyguard = isKeyguardState(); |
| boolean onKeyguardAndExpanded = onKeyguard && !mShowCollapsedOnKeyguard; |
| if (!mHeaderAnimating && !headerWillBeAnimating() && !mOverScrolling) { |
| getView().setTranslationY( |
| onKeyguardAndExpanded |
| ? translationScaleY * mHeader.getHeight() |
| : headerTranslation); |
| } |
| int currentHeight = getView().getHeight(); |
| if (expansion == mLastQSExpansion |
| && mLastKeyguardAndExpanded == onKeyguardAndExpanded |
| && mLastViewHeight == currentHeight |
| && mLastHeaderTranslation == headerTranslation |
| && mSquishinessFraction == squishinessFraction |
| && mLastPanelFraction == panelExpansionFraction) { |
| return; |
| } |
| mLastHeaderTranslation = headerTranslation; |
| mLastPanelFraction = panelExpansionFraction; |
| mSquishinessFraction = squishinessFraction; |
| mLastQSExpansion = expansion; |
| mLastKeyguardAndExpanded = onKeyguardAndExpanded; |
| mLastViewHeight = currentHeight; |
| |
| boolean fullyExpanded = expansion == 1; |
| boolean fullyCollapsed = expansion == 0.0f; |
| int heightDiff = getHeightDiff(); |
| float panelTranslationY = translationScaleY * heightDiff; |
| |
| if (expansion < 1 && expansion > 0.99) { |
| if (mQuickQSPanelController.switchTileLayout(false)) { |
| mHeader.updateResources(); |
| } |
| } |
| mQSPanelController.setIsOnKeyguard(onKeyguard); |
| mFooter.setExpansion(onKeyguardAndExpanded ? 1 : expansion); |
| float footerActionsExpansion = |
| onKeyguardAndExpanded ? 1 : mInSplitShade ? alphaProgress : expansion; |
| mQSFooterActionsViewModel.onQuickSettingsExpansionChanged(footerActionsExpansion, |
| mInSplitShade); |
| mQSPanelController.setRevealExpansion(expansion); |
| mQSPanelController.getTileLayout().setExpansion(expansion, proposedTranslation); |
| mQuickQSPanelController.getTileLayout().setExpansion(expansion, proposedTranslation); |
| |
| float qsScrollViewTranslation = |
| onKeyguard && !mShowCollapsedOnKeyguard ? panelTranslationY : 0; |
| mQSPanelScrollView.setTranslationY(qsScrollViewTranslation); |
| |
| if (fullyCollapsed) { |
| mQSPanelScrollView.setScrollY(0); |
| } |
| |
| if (!fullyExpanded) { |
| // Set bounds on the QS panel so it doesn't run over the header when animating. |
| mQsBounds.top = (int) -mQSPanelScrollView.getTranslationY(); |
| mQsBounds.right = mQSPanelScrollView.getWidth(); |
| mQsBounds.bottom = mQSPanelScrollView.getHeight(); |
| } |
| updateQsBounds(); |
| |
| if (mQSSquishinessController != null) { |
| mQSSquishinessController.setSquishiness(mSquishinessFraction); |
| } |
| if (mQSAnimator != null) { |
| mQSAnimator.setPosition(expansion); |
| } |
| if (!mInSplitShade |
| || mStatusBarStateController.getState() == KEYGUARD |
| || mStatusBarStateController.getState() == SHADE_LOCKED) { |
| // At beginning, state is 0 and will apply wrong squishiness to MediaHost in lockscreen |
| // and media player expect no change by squishiness in lock screen shade. Don't bother |
| // squishing mQsMediaHost when not in split shade to prevent problems with stale state. |
| mQsMediaHost.setSquishFraction(1.0F); |
| } else { |
| mQsMediaHost.setSquishFraction(mSquishinessFraction); |
| } |
| updateMediaPositions(); |
| } |
| |
| private void setAlphaAnimationProgress(float progress) { |
| final View view = getView(); |
| if (progress == 0 && view.getVisibility() != View.INVISIBLE) { |
| mLogger.logVisibility("QS fragment", View.INVISIBLE); |
| view.setVisibility(View.INVISIBLE); |
| } else if (progress > 0 && view.getVisibility() != View.VISIBLE) { |
| mLogger.logVisibility("QS fragment", View.VISIBLE); |
| view.setVisibility((View.VISIBLE)); |
| } |
| view.setAlpha(interpolateAlphaAnimationProgress(progress)); |
| } |
| |
| private float calculateAlphaProgress(float panelExpansionFraction) { |
| if (mIsSmallScreen) { |
| // Small screens. QS alpha is not animated. |
| return 1; |
| } |
| if (mInSplitShade) { |
| // Large screens in landscape. |
| // Need to check upcoming state as for unlocked -> AOD transition current state is |
| // not updated yet, but we're transitioning and UI should already follow KEYGUARD state |
| if (mTransitioningToFullShade |
| || mStatusBarStateController.getCurrentOrUpcomingState() == KEYGUARD) { |
| // Always use "mFullShadeProgress" on keyguard, because |
| // "panelExpansionFractions" is always 1 on keyguard split shade. |
| return mLockscreenToShadeProgress; |
| } else { |
| return panelExpansionFraction; |
| } |
| } |
| // Large screens in portrait. |
| if (mTransitioningToFullShade) { |
| // Only use this value during the standard lock screen shade expansion. During the |
| // "quick" expansion from top, this value is 0. |
| return mLockscreenToShadeProgress; |
| } else { |
| return panelExpansionFraction; |
| } |
| } |
| |
| private float interpolateAlphaAnimationProgress(float progress) { |
| if (mQSPanelController.isBouncerInTransit()) { |
| return BouncerPanelExpansionCalculator.aboutToShowBouncerProgress(progress); |
| } |
| if (isKeyguardState()) { |
| // Alpha progress should be linear on lockscreen shade expansion. |
| return progress; |
| } |
| if (mIsSmallScreen || !mFeatureFlags.isEnabled( |
| Flags.LARGE_SHADE_GRANULAR_ALPHA_INTERPOLATION)) { |
| return ShadeInterpolation.getContentAlpha(progress); |
| } else { |
| return mLargeScreenShadeInterpolator.getQsAlpha(progress); |
| } |
| } |
| |
| @VisibleForTesting |
| void updateQsBounds() { |
| if (mLastQSExpansion == 1.0f) { |
| // Fully expanded, let's set the layout bounds as clip bounds. This is necessary because |
| // it's a scrollview and otherwise wouldn't be clipped. However, we set the horizontal |
| // bounds so the pages go to the ends of QSContainerImpl (most cases) or its parent |
| // (large screen portrait) |
| int sideMargin = getResources().getDimensionPixelSize( |
| R.dimen.qs_tiles_page_horizontal_margin) * 2; |
| mQsBounds.set(-sideMargin, 0, mQSPanelScrollView.getWidth() + sideMargin, |
| mQSPanelScrollView.getHeight()); |
| } |
| mQSPanelScrollView.setClipBounds(mQsBounds); |
| |
| mQSPanelScrollView.getLocationOnScreen(mLocationTemp); |
| int left = mLocationTemp[0]; |
| int top = mLocationTemp[1]; |
| mQsMediaHost.getCurrentClipping().set(left, top, |
| left + getView().getMeasuredWidth(), |
| top + mQSPanelScrollView.getMeasuredHeight() |
| - mQSPanelController.getPaddingBottom()); |
| } |
| |
| private void updateMediaPositions() { |
| if (Utils.useQsMediaPlayer(getContext())) { |
| View hostView = mQsMediaHost.getHostView(); |
| // Make sure the media appears a bit from the top to make it look nicer |
| if (mLastQSExpansion > 0 && !isKeyguardState() && !mQqsMediaHost.getVisible() |
| && !mQSPanelController.shouldUseHorizontalLayout() && !mInSplitShade) { |
| float interpolation = 1.0f - mLastQSExpansion; |
| interpolation = Interpolators.ACCELERATE.getInterpolation(interpolation); |
| float translationY = -hostView.getHeight() * 1.3f * interpolation; |
| hostView.setTranslationY(translationY); |
| } else { |
| hostView.setTranslationY(0); |
| } |
| } |
| } |
| |
| private boolean headerWillBeAnimating() { |
| return mStatusBarState == KEYGUARD && mShowCollapsedOnKeyguard && !isKeyguardState(); |
| } |
| |
| @Override |
| public void animateHeaderSlidingOut() { |
| if (DEBUG) Log.d(TAG, "animateHeaderSlidingOut"); |
| if (getView().getY() == -mHeader.getHeight()) { |
| return; |
| } |
| mHeaderAnimating = true; |
| getView().animate().y(-mHeader.getHeight()) |
| .setStartDelay(0) |
| .setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD) |
| .setInterpolator(Interpolators.FAST_OUT_SLOW_IN) |
| .setListener(new AnimatorListenerAdapter() { |
| @Override |
| public void onAnimationEnd(Animator animation) { |
| if (getView() != null) { |
| // The view could be destroyed before the animation completes when |
| // switching users. |
| getView().animate().setListener(null); |
| } |
| mHeaderAnimating = false; |
| updateQsState(); |
| } |
| }) |
| .start(); |
| } |
| |
| @Override |
| public void setCollapseExpandAction(Runnable action) { |
| mQSPanelController.setCollapseExpandAction(action); |
| mQuickQSPanelController.setCollapseExpandAction(action); |
| } |
| |
| @Override |
| public void closeDetail() { |
| mQSPanelController.closeDetail(); |
| } |
| |
| @Override |
| public void closeCustomizer() { |
| mQSCustomizerController.hide(); |
| } |
| |
| public void notifyCustomizeChanged() { |
| // The customize state changed, so our height changed. |
| mContainer.updateExpansion(); |
| boolean customizing = isCustomizing(); |
| mQSPanelScrollView.setVisibility(!customizing ? View.VISIBLE : View.INVISIBLE); |
| mFooter.setVisibility(!customizing ? View.VISIBLE : View.INVISIBLE); |
| mQSFooterActionsViewModel.onVisibilityChangeRequested(!customizing); |
| mHeader.setVisibility(!customizing ? View.VISIBLE : View.INVISIBLE); |
| // Let the panel know the position changed and it needs to update where notifications |
| // and whatnot are. |
| mPanelView.onQsHeightChanged(); |
| } |
| |
| /** |
| * The height this view wants to be. This is different from {@link View#getMeasuredHeight} such |
| * that during closing the detail panel, this already returns the smaller height. |
| */ |
| @Override |
| public int getDesiredHeight() { |
| if (mQSCustomizerController.isCustomizing()) { |
| return getView().getHeight(); |
| } |
| return getView().getMeasuredHeight(); |
| } |
| |
| @Override |
| public void setHeightOverride(int desiredHeight) { |
| mContainer.setHeightOverride(desiredHeight); |
| } |
| |
| @Override |
| public int getQsMinExpansionHeight() { |
| if (mInSplitShade) { |
| return getQsMinExpansionHeightForSplitShade(); |
| } |
| return mHeader.getHeight(); |
| } |
| |
| /** |
| * Returns the min expansion height for split shade. |
| * |
| * On split shade, QS is always expanded and goes from the top of the screen to the bottom of |
| * the QS container. |
| */ |
| private int getQsMinExpansionHeightForSplitShade() { |
| getView().getLocationOnScreen(mLocationTemp); |
| int top = mLocationTemp[1]; |
| // We want to get the original top position, so we subtract any translation currently set. |
| int originalTop = (int) (top - getView().getTranslationY()); |
| // On split shade the QS view doesn't start at the top of the screen, so we need to add the |
| // top margin. |
| return originalTop + getView().getHeight(); |
| } |
| |
| @Override |
| public void hideImmediately() { |
| getView().animate().cancel(); |
| getView().setY(-getQsMinExpansionHeight()); |
| } |
| |
| private final ViewTreeObserver.OnPreDrawListener mStartHeaderSlidingIn |
| = new ViewTreeObserver.OnPreDrawListener() { |
| @Override |
| public boolean onPreDraw() { |
| getView().getViewTreeObserver().removeOnPreDrawListener(this); |
| getView().animate() |
| .translationY(0f) |
| .setDuration(StackStateAnimator.ANIMATION_DURATION_GO_TO_FULL_SHADE) |
| .setInterpolator(Interpolators.FAST_OUT_SLOW_IN) |
| .setListener(mAnimateHeaderSlidingInListener) |
| .start(); |
| return true; |
| } |
| }; |
| |
| private final Animator.AnimatorListener mAnimateHeaderSlidingInListener |
| = new AnimatorListenerAdapter() { |
| @Override |
| public void onAnimationEnd(Animator animation) { |
| mHeaderAnimating = false; |
| updateQsState(); |
| // Unset the listener, otherwise this may persist for another view property animation |
| getView().animate().setListener(null); |
| } |
| }; |
| |
| @Override |
| public void onUpcomingStateChanged(int upcomingState) { |
| if (upcomingState == KEYGUARD) { |
| // refresh state of QS as soon as possible - while it's still upcoming - so in case of |
| // transition to KEYGUARD (e.g. from unlocked to AOD) all objects are aware they should |
| // already behave like on keyguard. Otherwise we might be doing extra work, |
| // e.g. QSAnimator making QS visible and then quickly invisible |
| onStateChanged(upcomingState); |
| } |
| } |
| |
| @Override |
| public void onStateChanged(int newState) { |
| if (newState == mStatusBarState) { |
| return; |
| } |
| mStatusBarState = newState; |
| setKeyguardShowing(newState == KEYGUARD); |
| updateShowCollapsedOnKeyguard(); |
| } |
| |
| @VisibleForTesting |
| public ListeningAndVisibilityLifecycleOwner getListeningAndVisibilityLifecycleOwner() { |
| return mListeningAndVisibilityLifecycleOwner; |
| } |
| |
| @Override |
| public void dump(PrintWriter pw, String[] args) { |
| IndentingPrintWriter indentingPw = new IndentingPrintWriter(pw, /* singleIndent= */ " "); |
| indentingPw.println("QSFragment:"); |
| indentingPw.increaseIndent(); |
| indentingPw.println("mQsBounds: " + mQsBounds); |
| indentingPw.println("mQsExpanded: " + mQsExpanded); |
| indentingPw.println("mHeaderAnimating: " + mHeaderAnimating); |
| indentingPw.println("mStackScrollerOverscrolling: " + mStackScrollerOverscrolling); |
| indentingPw.println("mListening: " + mListening); |
| indentingPw.println("mQsVisible: " + mQsVisible); |
| indentingPw.println("mLayoutDirection: " + mLayoutDirection); |
| indentingPw.println("mLastQSExpansion: " + mLastQSExpansion); |
| indentingPw.println("mLastPanelFraction: " + mLastPanelFraction); |
| indentingPw.println("mSquishinessFraction: " + mSquishinessFraction); |
| indentingPw.println("mQsDisabled: " + mQsDisabled); |
| indentingPw.println("mTemp: " + Arrays.toString(mLocationTemp)); |
| indentingPw.println("mShowCollapsedOnKeyguard: " + mShowCollapsedOnKeyguard); |
| indentingPw.println("mLastKeyguardAndExpanded: " + mLastKeyguardAndExpanded); |
| indentingPw.println("mStatusBarState: " + StatusBarState.toString(mStatusBarState)); |
| indentingPw.println("mTmpLocation: " + Arrays.toString(mTmpLocation)); |
| indentingPw.println("mLastViewHeight: " + mLastViewHeight); |
| indentingPw.println("mLastHeaderTranslation: " + mLastHeaderTranslation); |
| indentingPw.println("mInSplitShade: " + mInSplitShade); |
| indentingPw.println("mTransitioningToFullShade: " + mTransitioningToFullShade); |
| indentingPw.println("mLockscreenToShadeProgress: " + mLockscreenToShadeProgress); |
| indentingPw.println("mOverScrolling: " + mOverScrolling); |
| indentingPw.println("isCustomizing: " + mQSCustomizerController.isCustomizing()); |
| View view = getView(); |
| if (view != null) { |
| indentingPw.println("top: " + view.getTop()); |
| indentingPw.println("y: " + view.getY()); |
| indentingPw.println("translationY: " + view.getTranslationY()); |
| indentingPw.println("alpha: " + view.getAlpha()); |
| indentingPw.println("height: " + view.getHeight()); |
| indentingPw.println("measuredHeight: " + view.getMeasuredHeight()); |
| indentingPw.println("clipBounds: " + view.getClipBounds()); |
| } else { |
| indentingPw.println("getView(): null"); |
| } |
| QuickStatusBarHeader header = mHeader; |
| if (header != null) { |
| indentingPw.println("headerHeight: " + header.getHeight()); |
| indentingPw.println("Header visibility: " + visibilityToString(header.getVisibility())); |
| } else { |
| indentingPw.println("mHeader: null"); |
| } |
| } |
| |
| private static String visibilityToString(int visibility) { |
| if (visibility == View.VISIBLE) { |
| return "VISIBLE"; |
| } |
| if (visibility == View.INVISIBLE) { |
| return "INVISIBLE"; |
| } |
| return "GONE"; |
| } |
| |
| /** |
| * A {@link LifecycleOwner} whose state is driven by the current state of this fragment: |
| * |
| * - DESTROYED when the fragment is destroyed. |
| * - CREATED when mListening == mQsVisible == false. |
| * - STARTED when mListening == true && mQsVisible == false. |
| * - RESUMED when mListening == true && mQsVisible == true. |
| */ |
| @VisibleForTesting |
| class ListeningAndVisibilityLifecycleOwner implements LifecycleOwner { |
| private final LifecycleRegistry mLifecycleRegistry = new LifecycleRegistry(this); |
| private boolean mDestroyed = false; |
| |
| { |
| updateState(); |
| } |
| |
| @Override |
| public Lifecycle getLifecycle() { |
| return mLifecycleRegistry; |
| } |
| |
| /** |
| * Update the state of the associated lifecycle. This should be called whenever |
| * {@code mListening} or {@code mQsVisible} is changed. |
| */ |
| public void updateState() { |
| if (mDestroyed) { |
| mLifecycleRegistry.setCurrentState(Lifecycle.State.DESTROYED); |
| return; |
| } |
| |
| if (!mListening) { |
| mLifecycleRegistry.setCurrentState(Lifecycle.State.CREATED); |
| return; |
| } |
| |
| // mListening && !mQsVisible. |
| if (!mQsVisible) { |
| mLifecycleRegistry.setCurrentState(Lifecycle.State.STARTED); |
| return; |
| } |
| |
| // mListening && mQsVisible. |
| mLifecycleRegistry.setCurrentState(Lifecycle.State.RESUMED); |
| } |
| |
| public void destroy() { |
| mDestroyed = true; |
| updateState(); |
| } |
| } |
| } |