Home | History | Annotate | Download | only in qs
      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 android.animation.Animator;
     20 import android.animation.AnimatorListenerAdapter;
     21 import android.content.Context;
     22 import android.content.res.Configuration;
     23 import android.graphics.Rect;
     24 import android.os.Bundle;
     25 import android.util.Log;
     26 import android.view.ContextThemeWrapper;
     27 import android.view.LayoutInflater;
     28 import android.view.MotionEvent;
     29 import android.view.View;
     30 import android.view.View.OnClickListener;
     31 import android.view.ViewGroup;
     32 import android.view.ViewTreeObserver;
     33 import android.widget.FrameLayout.LayoutParams;
     34 
     35 import androidx.annotation.Nullable;
     36 import androidx.annotation.VisibleForTesting;
     37 
     38 import com.android.systemui.Interpolators;
     39 import com.android.systemui.R;
     40 import com.android.systemui.R.id;
     41 import com.android.systemui.SysUiServiceProvider;
     42 import com.android.systemui.plugins.qs.QS;
     43 import com.android.systemui.qs.customize.QSCustomizer;
     44 import com.android.systemui.statusbar.CommandQueue;
     45 import com.android.systemui.statusbar.notification.stack.StackStateAnimator;
     46 import com.android.systemui.statusbar.phone.NotificationsQuickSettingsContainer;
     47 import com.android.systemui.statusbar.policy.RemoteInputQuickSettingsDisabler;
     48 import com.android.systemui.util.InjectionInflationController;
     49 import com.android.systemui.util.LifecycleFragment;
     50 
     51 import javax.inject.Inject;
     52 
     53 public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Callbacks {
     54     private static final String TAG = "QS";
     55     private static final boolean DEBUG = false;
     56     private static final String EXTRA_EXPANDED = "expanded";
     57     private static final String EXTRA_LISTENING = "listening";
     58 
     59     private final Rect mQsBounds = new Rect();
     60     private boolean mQsExpanded;
     61     private boolean mHeaderAnimating;
     62     private boolean mKeyguardShowing;
     63     private boolean mStackScrollerOverscrolling;
     64 
     65     private long mDelay;
     66 
     67     private QSAnimator mQSAnimator;
     68     private HeightListener mPanelView;
     69     protected QuickStatusBarHeader mHeader;
     70     private QSCustomizer mQSCustomizer;
     71     protected QSPanel mQSPanel;
     72     private QSDetail mQSDetail;
     73     private boolean mListening;
     74     private QSContainerImpl mContainer;
     75     private int mLayoutDirection;
     76     private QSFooter mFooter;
     77     private float mLastQSExpansion = -1;
     78     private boolean mQsDisabled;
     79 
     80     private final RemoteInputQuickSettingsDisabler mRemoteInputQuickSettingsDisabler;
     81     private final InjectionInflationController mInjectionInflater;
     82     private final QSTileHost mHost;
     83 
     84     @Inject
     85     public QSFragment(RemoteInputQuickSettingsDisabler remoteInputQsDisabler,
     86             InjectionInflationController injectionInflater,
     87             Context context,
     88             QSTileHost qsTileHost) {
     89         mRemoteInputQuickSettingsDisabler = remoteInputQsDisabler;
     90         mInjectionInflater = injectionInflater;
     91         SysUiServiceProvider.getComponent(context, CommandQueue.class)
     92                 .observe(getLifecycle(), this);
     93         mHost = qsTileHost;
     94     }
     95 
     96     @Override
     97     public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
     98             Bundle savedInstanceState) {
     99         inflater = mInjectionInflater.injectable(
    100                 inflater.cloneInContext(new ContextThemeWrapper(getContext(), R.style.qs_theme)));
    101         return inflater.inflate(R.layout.qs_panel, container, false);
    102     }
    103 
    104     @Override
    105     public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
    106         super.onViewCreated(view, savedInstanceState);
    107         mQSPanel = view.findViewById(R.id.quick_settings_panel);
    108         mQSDetail = view.findViewById(R.id.qs_detail);
    109         mHeader = view.findViewById(R.id.header);
    110         mFooter = view.findViewById(R.id.qs_footer);
    111         mContainer = view.findViewById(id.quick_settings_container);
    112 
    113         mQSDetail.setQsPanel(mQSPanel, mHeader, (View) mFooter);
    114         mQSAnimator = new QSAnimator(this,
    115                 mHeader.findViewById(R.id.quick_qs_panel), mQSPanel);
    116 
    117         mQSCustomizer = view.findViewById(R.id.qs_customize);
    118         mQSCustomizer.setQs(this);
    119         if (savedInstanceState != null) {
    120             setExpanded(savedInstanceState.getBoolean(EXTRA_EXPANDED));
    121             setListening(savedInstanceState.getBoolean(EXTRA_LISTENING));
    122             setEditLocation(view);
    123             mQSCustomizer.restoreInstanceState(savedInstanceState);
    124             if (mQsExpanded) {
    125                 mQSPanel.getTileLayout().restoreInstanceState(savedInstanceState);
    126             }
    127         }
    128         setHost(mHost);
    129     }
    130 
    131     @Override
    132     public void onDestroy() {
    133         super.onDestroy();
    134         if (mListening) {
    135             setListening(false);
    136         }
    137     }
    138 
    139     @Override
    140     public void onSaveInstanceState(Bundle outState) {
    141         super.onSaveInstanceState(outState);
    142         outState.putBoolean(EXTRA_EXPANDED, mQsExpanded);
    143         outState.putBoolean(EXTRA_LISTENING, mListening);
    144         mQSCustomizer.saveInstanceState(outState);
    145         if (mQsExpanded) {
    146             mQSPanel.getTileLayout().saveInstanceState(outState);
    147         }
    148     }
    149 
    150     @VisibleForTesting
    151     boolean isListening() {
    152         return mListening;
    153     }
    154 
    155     @VisibleForTesting
    156     boolean isExpanded() {
    157         return mQsExpanded;
    158     }
    159 
    160     @Override
    161     public View getHeader() {
    162         return mHeader;
    163     }
    164 
    165     @Override
    166     public void setHasNotifications(boolean hasNotifications) {
    167     }
    168 
    169     @Override
    170     public void setPanelView(HeightListener panelView) {
    171         mPanelView = panelView;
    172     }
    173 
    174     @Override
    175     public void onConfigurationChanged(Configuration newConfig) {
    176         super.onConfigurationChanged(newConfig);
    177         setEditLocation(getView());
    178         if (newConfig.getLayoutDirection() != mLayoutDirection) {
    179             mLayoutDirection = newConfig.getLayoutDirection();
    180             if (mQSAnimator != null) {
    181                 mQSAnimator.onRtlChanged();
    182             }
    183         }
    184     }
    185 
    186     private void setEditLocation(View view) {
    187         View edit = view.findViewById(android.R.id.edit);
    188         int[] loc = edit.getLocationOnScreen();
    189         int x = loc[0] + edit.getWidth() / 2;
    190         int y = loc[1] + edit.getHeight() / 2;
    191         mQSCustomizer.setEditLocation(x, y);
    192     }
    193 
    194     @Override
    195     public void setContainer(ViewGroup container) {
    196         if (container instanceof NotificationsQuickSettingsContainer) {
    197             mQSCustomizer.setContainer((NotificationsQuickSettingsContainer) container);
    198         }
    199     }
    200 
    201     @Override
    202     public boolean isCustomizing() {
    203         return mQSCustomizer.isCustomizing();
    204     }
    205 
    206     public void setHost(QSTileHost qsh) {
    207         mQSPanel.setHost(qsh, mQSCustomizer);
    208         mHeader.setQSPanel(mQSPanel);
    209         mFooter.setQSPanel(mQSPanel);
    210         mQSDetail.setHost(qsh);
    211 
    212         if (mQSAnimator != null) {
    213             mQSAnimator.setHost(qsh);
    214         }
    215     }
    216 
    217     @Override
    218     public void disable(int displayId, int state1, int state2, boolean animate) {
    219         if (displayId != getContext().getDisplayId()) {
    220             return;
    221         }
    222         state2 = mRemoteInputQuickSettingsDisabler.adjustDisableFlags(state2);
    223 
    224         final boolean disabled = (state2 & DISABLE2_QUICK_SETTINGS) != 0;
    225         if (disabled == mQsDisabled) return;
    226         mQsDisabled = disabled;
    227         mContainer.disable(state1, state2, animate);
    228         mHeader.disable(state1, state2, animate);
    229         mFooter.disable(state1, state2, animate);
    230         updateQsState();
    231     }
    232 
    233     private void updateQsState() {
    234         final boolean expandVisually = mQsExpanded || mStackScrollerOverscrolling
    235                 || mHeaderAnimating;
    236         mQSPanel.setExpanded(mQsExpanded);
    237         mQSDetail.setExpanded(mQsExpanded);
    238         mHeader.setVisibility((mQsExpanded || !mKeyguardShowing || mHeaderAnimating)
    239                 ? View.VISIBLE
    240                 : View.INVISIBLE);
    241         mHeader.setExpanded((mKeyguardShowing && !mHeaderAnimating)
    242                 || (mQsExpanded && !mStackScrollerOverscrolling));
    243         mFooter.setVisibility(
    244                 !mQsDisabled && (mQsExpanded || !mKeyguardShowing || mHeaderAnimating)
    245                 ? View.VISIBLE
    246                 : View.INVISIBLE);
    247         mFooter.setExpanded((mKeyguardShowing && !mHeaderAnimating)
    248                 || (mQsExpanded && !mStackScrollerOverscrolling));
    249         mQSPanel.setVisibility(!mQsDisabled && expandVisually ? View.VISIBLE : View.INVISIBLE);
    250     }
    251 
    252     public QSPanel getQsPanel() {
    253         return mQSPanel;
    254     }
    255 
    256     public QSCustomizer getCustomizer() {
    257         return mQSCustomizer;
    258     }
    259 
    260     @Override
    261     public boolean isShowingDetail() {
    262         return mQSPanel.isShowingCustomize() || mQSDetail.isShowingDetail();
    263     }
    264 
    265     @Override
    266     public boolean onInterceptTouchEvent(MotionEvent event) {
    267         return isCustomizing();
    268     }
    269 
    270     @Override
    271     public void setHeaderClickable(boolean clickable) {
    272         if (DEBUG) Log.d(TAG, "setHeaderClickable " + clickable);
    273     }
    274 
    275     @Override
    276     public void setExpanded(boolean expanded) {
    277         if (DEBUG) Log.d(TAG, "setExpanded " + expanded);
    278         mQsExpanded = expanded;
    279         mQSPanel.setListening(mListening, mQsExpanded);
    280         updateQsState();
    281     }
    282 
    283     @Override
    284     public void setKeyguardShowing(boolean keyguardShowing) {
    285         if (DEBUG) Log.d(TAG, "setKeyguardShowing " + keyguardShowing);
    286         mKeyguardShowing = keyguardShowing;
    287         mLastQSExpansion = -1;
    288 
    289         if (mQSAnimator != null) {
    290             mQSAnimator.setOnKeyguard(keyguardShowing);
    291         }
    292 
    293         mFooter.setKeyguardShowing(keyguardShowing);
    294         updateQsState();
    295     }
    296 
    297     @Override
    298     public void setOverscrolling(boolean stackScrollerOverscrolling) {
    299         if (DEBUG) Log.d(TAG, "setOverscrolling " + stackScrollerOverscrolling);
    300         mStackScrollerOverscrolling = stackScrollerOverscrolling;
    301         updateQsState();
    302     }
    303 
    304     @Override
    305     public void setListening(boolean listening) {
    306         if (DEBUG) Log.d(TAG, "setListening " + listening);
    307         mListening = listening;
    308         mHeader.setListening(listening);
    309         mFooter.setListening(listening);
    310         mQSPanel.setListening(mListening, mQsExpanded);
    311     }
    312 
    313     @Override
    314     public void setHeaderListening(boolean listening) {
    315         mHeader.setListening(listening);
    316         mFooter.setListening(listening);
    317     }
    318 
    319     @Override
    320     public void setQsExpansion(float expansion, float headerTranslation) {
    321         if (DEBUG) Log.d(TAG, "setQSExpansion " + expansion + " " + headerTranslation);
    322         mContainer.setExpansion(expansion);
    323         final float translationScaleY = expansion - 1;
    324         if (!mHeaderAnimating) {
    325             getView().setTranslationY(
    326                     mKeyguardShowing
    327                             ? translationScaleY * mHeader.getHeight()
    328                             : headerTranslation);
    329         }
    330         if (expansion == mLastQSExpansion) {
    331             return;
    332         }
    333         mLastQSExpansion = expansion;
    334 
    335         boolean fullyExpanded = expansion == 1;
    336         int heightDiff = mQSPanel.getBottom() - mHeader.getBottom() + mHeader.getPaddingBottom()
    337                 + mFooter.getHeight();
    338         float panelTranslationY = translationScaleY * heightDiff;
    339 
    340         // Let the views animate their contents correctly by giving them the necessary context.
    341         mHeader.setExpansion(mKeyguardShowing, expansion, panelTranslationY);
    342         mFooter.setExpansion(mKeyguardShowing ? 1 : expansion);
    343         mQSPanel.getQsTileRevealController().setExpansion(expansion);
    344         mQSPanel.getTileLayout().setExpansion(expansion);
    345         mQSPanel.setTranslationY(translationScaleY * heightDiff);
    346         mQSDetail.setFullyExpanded(fullyExpanded);
    347 
    348         if (fullyExpanded) {
    349             // Always draw within the bounds of the view when fully expanded.
    350             mQSPanel.setClipBounds(null);
    351         } else {
    352             // Set bounds on the QS panel so it doesn't run over the header when animating.
    353             mQsBounds.top = (int) -mQSPanel.getTranslationY();
    354             mQsBounds.right = mQSPanel.getWidth();
    355             mQsBounds.bottom = mQSPanel.getHeight();
    356             mQSPanel.setClipBounds(mQsBounds);
    357         }
    358 
    359         if (mQSAnimator != null) {
    360             mQSAnimator.setPosition(expansion);
    361         }
    362     }
    363 
    364     @Override
    365     public void animateHeaderSlidingIn(long delay) {
    366         if (DEBUG) Log.d(TAG, "animateHeaderSlidingIn");
    367         // If the QS is already expanded we don't need to slide in the header as it's already
    368         // visible.
    369         if (!mQsExpanded) {
    370             mHeaderAnimating = true;
    371             mDelay = delay;
    372             getView().getViewTreeObserver().addOnPreDrawListener(mStartHeaderSlidingIn);
    373         }
    374     }
    375 
    376     @Override
    377     public void animateHeaderSlidingOut() {
    378         if (DEBUG) Log.d(TAG, "animateHeaderSlidingOut");
    379         mHeaderAnimating = true;
    380         getView().animate().y(-mHeader.getHeight())
    381                 .setStartDelay(0)
    382                 .setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD)
    383                 .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
    384                 .setListener(new AnimatorListenerAdapter() {
    385                     @Override
    386                     public void onAnimationEnd(Animator animation) {
    387                         if (getView() != null) {
    388                             // The view could be destroyed before the animation completes when
    389                             // switching users.
    390                             getView().animate().setListener(null);
    391                         }
    392                         mHeaderAnimating = false;
    393                         updateQsState();
    394                     }
    395                 })
    396                 .start();
    397     }
    398 
    399     @Override
    400     public void setExpandClickListener(OnClickListener onClickListener) {
    401         mFooter.setExpandClickListener(onClickListener);
    402     }
    403 
    404     @Override
    405     public void closeDetail() {
    406         mQSPanel.closeDetail();
    407     }
    408 
    409     public void notifyCustomizeChanged() {
    410         // The customize state changed, so our height changed.
    411         mContainer.updateExpansion();
    412         mQSPanel.setVisibility(!mQSCustomizer.isCustomizing() ? View.VISIBLE : View.INVISIBLE);
    413         mFooter.setVisibility(!mQSCustomizer.isCustomizing() ? View.VISIBLE : View.INVISIBLE);
    414         // Let the panel know the position changed and it needs to update where notifications
    415         // and whatnot are.
    416         mPanelView.onQsHeightChanged();
    417     }
    418 
    419     /**
    420      * The height this view wants to be. This is different from {@link #getMeasuredHeight} such that
    421      * during closing the detail panel, this already returns the smaller height.
    422      */
    423     @Override
    424     public int getDesiredHeight() {
    425         if (mQSCustomizer.isCustomizing()) {
    426             return getView().getHeight();
    427         }
    428         if (mQSDetail.isClosingDetail()) {
    429             LayoutParams layoutParams = (LayoutParams) mQSPanel.getLayoutParams();
    430             int panelHeight = layoutParams.topMargin + layoutParams.bottomMargin +
    431                     + mQSPanel.getMeasuredHeight();
    432             return panelHeight + getView().getPaddingBottom();
    433         } else {
    434             return getView().getMeasuredHeight();
    435         }
    436     }
    437 
    438     @Override
    439     public void setHeightOverride(int desiredHeight) {
    440         mContainer.setHeightOverride(desiredHeight);
    441     }
    442 
    443     @Override
    444     public int getQsMinExpansionHeight() {
    445         return mHeader.getHeight();
    446     }
    447 
    448     @Override
    449     public void hideImmediately() {
    450         getView().animate().cancel();
    451         getView().setY(-mHeader.getHeight());
    452     }
    453 
    454     private final ViewTreeObserver.OnPreDrawListener mStartHeaderSlidingIn
    455             = new ViewTreeObserver.OnPreDrawListener() {
    456         @Override
    457         public boolean onPreDraw() {
    458             getView().getViewTreeObserver().removeOnPreDrawListener(this);
    459             getView().animate()
    460                     .translationY(0f)
    461                     .setStartDelay(mDelay)
    462                     .setDuration(StackStateAnimator.ANIMATION_DURATION_GO_TO_FULL_SHADE)
    463                     .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
    464                     .setListener(mAnimateHeaderSlidingInListener)
    465                     .start();
    466             getView().setY(-mHeader.getHeight());
    467             return true;
    468         }
    469     };
    470 
    471     private final Animator.AnimatorListener mAnimateHeaderSlidingInListener
    472             = new AnimatorListenerAdapter() {
    473         @Override
    474         public void onAnimationEnd(Animator animation) {
    475             mHeaderAnimating = false;
    476             updateQsState();
    477         }
    478     };
    479 }
    480