/*
|
* 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 android.animation.Animator;
|
import android.animation.AnimatorListenerAdapter;
|
import android.content.Context;
|
import android.content.res.Configuration;
|
import android.graphics.Rect;
|
import android.os.Bundle;
|
import android.util.Log;
|
import android.view.ContextThemeWrapper;
|
import android.view.LayoutInflater;
|
import android.view.MotionEvent;
|
import android.view.View;
|
import android.view.View.OnClickListener;
|
import android.view.ViewGroup;
|
import android.view.ViewTreeObserver;
|
import android.widget.FrameLayout.LayoutParams;
|
|
import androidx.annotation.Nullable;
|
import androidx.annotation.VisibleForTesting;
|
|
import com.android.systemui.Interpolators;
|
import com.android.systemui.R;
|
import com.android.systemui.R.id;
|
import com.android.systemui.SysUiServiceProvider;
|
import com.android.systemui.plugins.qs.QS;
|
import com.android.systemui.qs.customize.QSCustomizer;
|
import com.android.systemui.statusbar.CommandQueue;
|
import com.android.systemui.statusbar.notification.stack.StackStateAnimator;
|
import com.android.systemui.statusbar.phone.NotificationsQuickSettingsContainer;
|
import com.android.systemui.statusbar.policy.RemoteInputQuickSettingsDisabler;
|
import com.android.systemui.util.InjectionInflationController;
|
import com.android.systemui.util.LifecycleFragment;
|
|
import javax.inject.Inject;
|
|
public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Callbacks {
|
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 final Rect mQsBounds = new Rect();
|
private boolean mQsExpanded;
|
private boolean mHeaderAnimating;
|
private boolean mKeyguardShowing;
|
private boolean mStackScrollerOverscrolling;
|
|
private long mDelay;
|
|
private QSAnimator mQSAnimator;
|
private HeightListener mPanelView;
|
protected QuickStatusBarHeader mHeader;
|
private QSCustomizer mQSCustomizer;
|
protected QSPanel mQSPanel;
|
private QSDetail mQSDetail;
|
private boolean mListening;
|
private QSContainerImpl mContainer;
|
private int mLayoutDirection;
|
private QSFooter mFooter;
|
private float mLastQSExpansion = -1;
|
private boolean mQsDisabled;
|
|
private final RemoteInputQuickSettingsDisabler mRemoteInputQuickSettingsDisabler;
|
private final InjectionInflationController mInjectionInflater;
|
private final QSTileHost mHost;
|
|
@Inject
|
public QSFragment(RemoteInputQuickSettingsDisabler remoteInputQsDisabler,
|
InjectionInflationController injectionInflater,
|
Context context,
|
QSTileHost qsTileHost) {
|
mRemoteInputQuickSettingsDisabler = remoteInputQsDisabler;
|
mInjectionInflater = injectionInflater;
|
SysUiServiceProvider.getComponent(context, CommandQueue.class)
|
.observe(getLifecycle(), this);
|
mHost = qsTileHost;
|
}
|
|
@Override
|
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
|
Bundle savedInstanceState) {
|
inflater = mInjectionInflater.injectable(
|
inflater.cloneInContext(new ContextThemeWrapper(getContext(), R.style.qs_theme)));
|
return inflater.inflate(R.layout.qs_panel, container, false);
|
}
|
|
@Override
|
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
|
super.onViewCreated(view, savedInstanceState);
|
mQSPanel = view.findViewById(R.id.quick_settings_panel);
|
mQSDetail = view.findViewById(R.id.qs_detail);
|
mHeader = view.findViewById(R.id.header);
|
mFooter = view.findViewById(R.id.qs_footer);
|
mContainer = view.findViewById(id.quick_settings_container);
|
|
mQSDetail.setQsPanel(mQSPanel, mHeader, (View) mFooter);
|
mQSAnimator = new QSAnimator(this,
|
mHeader.findViewById(R.id.quick_qs_panel), mQSPanel);
|
|
mQSCustomizer = view.findViewById(R.id.qs_customize);
|
mQSCustomizer.setQs(this);
|
if (savedInstanceState != null) {
|
setExpanded(savedInstanceState.getBoolean(EXTRA_EXPANDED));
|
setListening(savedInstanceState.getBoolean(EXTRA_LISTENING));
|
setEditLocation(view);
|
mQSCustomizer.restoreInstanceState(savedInstanceState);
|
if (mQsExpanded) {
|
mQSPanel.getTileLayout().restoreInstanceState(savedInstanceState);
|
}
|
}
|
setHost(mHost);
|
}
|
|
@Override
|
public void onDestroy() {
|
super.onDestroy();
|
if (mListening) {
|
setListening(false);
|
}
|
}
|
|
@Override
|
public void onSaveInstanceState(Bundle outState) {
|
super.onSaveInstanceState(outState);
|
outState.putBoolean(EXTRA_EXPANDED, mQsExpanded);
|
outState.putBoolean(EXTRA_LISTENING, mListening);
|
mQSCustomizer.saveInstanceState(outState);
|
if (mQsExpanded) {
|
mQSPanel.getTileLayout().saveInstanceState(outState);
|
}
|
}
|
|
@VisibleForTesting
|
boolean isListening() {
|
return mListening;
|
}
|
|
@VisibleForTesting
|
boolean isExpanded() {
|
return mQsExpanded;
|
}
|
|
@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();
|
}
|
}
|
}
|
|
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;
|
mQSCustomizer.setEditLocation(x, y);
|
}
|
|
@Override
|
public void setContainer(ViewGroup container) {
|
if (container instanceof NotificationsQuickSettingsContainer) {
|
mQSCustomizer.setContainer((NotificationsQuickSettingsContainer) container);
|
}
|
}
|
|
@Override
|
public boolean isCustomizing() {
|
return mQSCustomizer.isCustomizing();
|
}
|
|
public void setHost(QSTileHost qsh) {
|
mQSPanel.setHost(qsh, mQSCustomizer);
|
mHeader.setQSPanel(mQSPanel);
|
mFooter.setQSPanel(mQSPanel);
|
mQSDetail.setHost(qsh);
|
|
if (mQSAnimator != null) {
|
mQSAnimator.setHost(qsh);
|
}
|
}
|
|
@Override
|
public void disable(int displayId, int state1, int state2, boolean animate) {
|
if (displayId != getContext().getDisplayId()) {
|
return;
|
}
|
state2 = mRemoteInputQuickSettingsDisabler.adjustDisableFlags(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;
|
mQSPanel.setExpanded(mQsExpanded);
|
mQSDetail.setExpanded(mQsExpanded);
|
mHeader.setVisibility((mQsExpanded || !mKeyguardShowing || mHeaderAnimating)
|
? View.VISIBLE
|
: View.INVISIBLE);
|
mHeader.setExpanded((mKeyguardShowing && !mHeaderAnimating)
|
|| (mQsExpanded && !mStackScrollerOverscrolling));
|
mFooter.setVisibility(
|
!mQsDisabled && (mQsExpanded || !mKeyguardShowing || mHeaderAnimating)
|
? View.VISIBLE
|
: View.INVISIBLE);
|
mFooter.setExpanded((mKeyguardShowing && !mHeaderAnimating)
|
|| (mQsExpanded && !mStackScrollerOverscrolling));
|
mQSPanel.setVisibility(!mQsDisabled && expandVisually ? View.VISIBLE : View.INVISIBLE);
|
}
|
|
public QSPanel getQsPanel() {
|
return mQSPanel;
|
}
|
|
public QSCustomizer getCustomizer() {
|
return mQSCustomizer;
|
}
|
|
@Override
|
public boolean isShowingDetail() {
|
return mQSPanel.isShowingCustomize() || mQSDetail.isShowingDetail();
|
}
|
|
@Override
|
public boolean onInterceptTouchEvent(MotionEvent event) {
|
return 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;
|
mQSPanel.setListening(mListening, mQsExpanded);
|
updateQsState();
|
}
|
|
@Override
|
public void setKeyguardShowing(boolean keyguardShowing) {
|
if (DEBUG) Log.d(TAG, "setKeyguardShowing " + keyguardShowing);
|
mKeyguardShowing = 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;
|
mHeader.setListening(listening);
|
mFooter.setListening(listening);
|
mQSPanel.setListening(mListening, mQsExpanded);
|
}
|
|
@Override
|
public void setHeaderListening(boolean listening) {
|
mHeader.setListening(listening);
|
mFooter.setListening(listening);
|
}
|
|
@Override
|
public void setQsExpansion(float expansion, float headerTranslation) {
|
if (DEBUG) Log.d(TAG, "setQSExpansion " + expansion + " " + headerTranslation);
|
mContainer.setExpansion(expansion);
|
final float translationScaleY = expansion - 1;
|
if (!mHeaderAnimating) {
|
getView().setTranslationY(
|
mKeyguardShowing
|
? translationScaleY * mHeader.getHeight()
|
: headerTranslation);
|
}
|
if (expansion == mLastQSExpansion) {
|
return;
|
}
|
mLastQSExpansion = expansion;
|
|
boolean fullyExpanded = expansion == 1;
|
int heightDiff = mQSPanel.getBottom() - mHeader.getBottom() + mHeader.getPaddingBottom()
|
+ mFooter.getHeight();
|
float panelTranslationY = translationScaleY * heightDiff;
|
|
// Let the views animate their contents correctly by giving them the necessary context.
|
mHeader.setExpansion(mKeyguardShowing, expansion, panelTranslationY);
|
mFooter.setExpansion(mKeyguardShowing ? 1 : expansion);
|
mQSPanel.getQsTileRevealController().setExpansion(expansion);
|
mQSPanel.getTileLayout().setExpansion(expansion);
|
mQSPanel.setTranslationY(translationScaleY * heightDiff);
|
mQSDetail.setFullyExpanded(fullyExpanded);
|
|
if (fullyExpanded) {
|
// Always draw within the bounds of the view when fully expanded.
|
mQSPanel.setClipBounds(null);
|
} else {
|
// Set bounds on the QS panel so it doesn't run over the header when animating.
|
mQsBounds.top = (int) -mQSPanel.getTranslationY();
|
mQsBounds.right = mQSPanel.getWidth();
|
mQsBounds.bottom = mQSPanel.getHeight();
|
mQSPanel.setClipBounds(mQsBounds);
|
}
|
|
if (mQSAnimator != null) {
|
mQSAnimator.setPosition(expansion);
|
}
|
}
|
|
@Override
|
public void animateHeaderSlidingIn(long delay) {
|
if (DEBUG) Log.d(TAG, "animateHeaderSlidingIn");
|
// If the QS is already expanded we don't need to slide in the header as it's already
|
// visible.
|
if (!mQsExpanded) {
|
mHeaderAnimating = true;
|
mDelay = delay;
|
getView().getViewTreeObserver().addOnPreDrawListener(mStartHeaderSlidingIn);
|
}
|
}
|
|
@Override
|
public void animateHeaderSlidingOut() {
|
if (DEBUG) Log.d(TAG, "animateHeaderSlidingOut");
|
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 setExpandClickListener(OnClickListener onClickListener) {
|
mFooter.setExpandClickListener(onClickListener);
|
}
|
|
@Override
|
public void closeDetail() {
|
mQSPanel.closeDetail();
|
}
|
|
public void notifyCustomizeChanged() {
|
// The customize state changed, so our height changed.
|
mContainer.updateExpansion();
|
mQSPanel.setVisibility(!mQSCustomizer.isCustomizing() ? View.VISIBLE : View.INVISIBLE);
|
mFooter.setVisibility(!mQSCustomizer.isCustomizing() ? 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 #getMeasuredHeight} such that
|
* during closing the detail panel, this already returns the smaller height.
|
*/
|
@Override
|
public int getDesiredHeight() {
|
if (mQSCustomizer.isCustomizing()) {
|
return getView().getHeight();
|
}
|
if (mQSDetail.isClosingDetail()) {
|
LayoutParams layoutParams = (LayoutParams) mQSPanel.getLayoutParams();
|
int panelHeight = layoutParams.topMargin + layoutParams.bottomMargin +
|
+ mQSPanel.getMeasuredHeight();
|
return panelHeight + getView().getPaddingBottom();
|
} else {
|
return getView().getMeasuredHeight();
|
}
|
}
|
|
@Override
|
public void setHeightOverride(int desiredHeight) {
|
mContainer.setHeightOverride(desiredHeight);
|
}
|
|
@Override
|
public int getQsMinExpansionHeight() {
|
return mHeader.getHeight();
|
}
|
|
@Override
|
public void hideImmediately() {
|
getView().animate().cancel();
|
getView().setY(-mHeader.getHeight());
|
}
|
|
private final ViewTreeObserver.OnPreDrawListener mStartHeaderSlidingIn
|
= new ViewTreeObserver.OnPreDrawListener() {
|
@Override
|
public boolean onPreDraw() {
|
getView().getViewTreeObserver().removeOnPreDrawListener(this);
|
getView().animate()
|
.translationY(0f)
|
.setStartDelay(mDelay)
|
.setDuration(StackStateAnimator.ANIMATION_DURATION_GO_TO_FULL_SHADE)
|
.setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
|
.setListener(mAnimateHeaderSlidingInListener)
|
.start();
|
getView().setY(-mHeader.getHeight());
|
return true;
|
}
|
};
|
|
private final Animator.AnimatorListener mAnimateHeaderSlidingInListener
|
= new AnimatorListenerAdapter() {
|
@Override
|
public void onAnimationEnd(Animator animation) {
|
mHeaderAnimating = false;
|
updateQsState();
|
}
|
};
|
}
|