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 android.animation.Animator;
18 import android.animation.AnimatorListenerAdapter;
19 import android.annotation.Nullable;
20 import android.app.Fragment;
21 import android.content.res.Configuration;
22 import android.graphics.Rect;
23 import android.os.Bundle;
24 import android.support.annotation.VisibleForTesting;
25 import android.util.Log;
26 import android.view.ContextThemeWrapper;
27 import android.view.LayoutInflater;
28 import android.view.View;
29 import android.view.View.OnClickListener;
30 import android.view.ViewGroup;
31 import android.view.ViewTreeObserver;
32 import android.widget.FrameLayout.LayoutParams;
33 
34 import com.android.systemui.Interpolators;
35 import com.android.systemui.R;
36 import com.android.systemui.R.id;
37 import com.android.systemui.plugins.qs.QS;
38 import com.android.systemui.qs.customize.QSCustomizer;
39 import com.android.systemui.statusbar.phone.NotificationsQuickSettingsContainer;
40 import com.android.systemui.statusbar.stack.StackStateAnimator;
41 
42 public class QSFragment extends Fragment implements QS {
43     private static final String TAG = "QS";
44     private static final boolean DEBUG = false;
45     private static final String EXTRA_EXPANDED = "expanded";
46     private static final String EXTRA_LISTENING = "listening";
47 
48     private final Rect mQsBounds = new Rect();
49     private boolean mQsExpanded;
50     private boolean mHeaderAnimating;
51     private boolean mKeyguardShowing;
52     private boolean mStackScrollerOverscrolling;
53 
54     private long mDelay;
55 
56     private QSAnimator mQSAnimator;
57     private HeightListener mPanelView;
58     protected QuickStatusBarHeader mHeader;
59     private QSCustomizer mQSCustomizer;
60     protected QSPanel mQSPanel;
61     private QSDetail mQSDetail;
62     private boolean mListening;
63     private QSContainerImpl mContainer;
64     private int mLayoutDirection;
65     private QSFooter mFooter;
66     private int mGutterHeight;
67 
68     @Override
onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState)69     public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
70             Bundle savedInstanceState) {
71         inflater =inflater.cloneInContext(new ContextThemeWrapper(getContext(), R.style.qs_theme));
72         return inflater.inflate(R.layout.qs_panel, container, false);
73     }
74 
75     @Override
onViewCreated(View view, @Nullable Bundle savedInstanceState)76     public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
77         super.onViewCreated(view, savedInstanceState);
78         mQSPanel = view.findViewById(R.id.quick_settings_panel);
79         mQSDetail = view.findViewById(R.id.qs_detail);
80         mHeader = view.findViewById(R.id.header);
81         mFooter = view.findViewById(R.id.qs_footer);
82         mContainer = view.findViewById(id.quick_settings_container);
83         mGutterHeight = getContext().getResources().getDimensionPixelSize(R.dimen.qs_gutter_height);
84 
85         mQSDetail.setQsPanel(mQSPanel, mHeader, mFooter);
86 
87         // If the quick settings row is not shown, then there is no need for the animation from
88         // the row to the full QS panel.
89         if (getResources().getBoolean(R.bool.config_showQuickSettingsRow)) {
90             mQSAnimator = new QSAnimator(this,
91                     mHeader.findViewById(R.id.quick_qs_panel), mQSPanel);
92         }
93 
94         mQSCustomizer = view.findViewById(R.id.qs_customize);
95         mQSCustomizer.setQs(this);
96         if (savedInstanceState != null) {
97             setExpanded(savedInstanceState.getBoolean(EXTRA_EXPANDED));
98             setListening(savedInstanceState.getBoolean(EXTRA_LISTENING));
99         }
100     }
101 
102     @Override
onDestroy()103     public void onDestroy() {
104         super.onDestroy();
105         if (mListening) {
106             setListening(false);
107         }
108     }
109 
110     @Override
onSaveInstanceState(Bundle outState)111     public void onSaveInstanceState(Bundle outState) {
112         super.onSaveInstanceState(outState);
113         outState.putBoolean(EXTRA_EXPANDED, mQsExpanded);
114         outState.putBoolean(EXTRA_LISTENING, mListening);
115     }
116 
117     @VisibleForTesting
isListening()118     boolean isListening() {
119         return mListening;
120     }
121 
122     @VisibleForTesting
isExpanded()123     boolean isExpanded() {
124         return mQsExpanded;
125     }
126 
127     @Override
getHeader()128     public View getHeader() {
129         return mHeader;
130     }
131 
132     @Override
setHasNotifications(boolean hasNotifications)133     public void setHasNotifications(boolean hasNotifications) {
134         mContainer.setGutterEnabled(hasNotifications);
135     }
136 
setPanelView(HeightListener panelView)137     public void setPanelView(HeightListener panelView) {
138         mPanelView = panelView;
139     }
140 
141     @Override
onConfigurationChanged(Configuration newConfig)142     public void onConfigurationChanged(Configuration newConfig) {
143         super.onConfigurationChanged(newConfig);
144         if (newConfig.getLayoutDirection() != mLayoutDirection) {
145             mLayoutDirection = newConfig.getLayoutDirection();
146 
147             if (mQSAnimator != null) {
148                 mQSAnimator.onRtlChanged();
149             }
150         }
151     }
152 
153     @Override
setContainer(ViewGroup container)154     public void setContainer(ViewGroup container) {
155         if (container instanceof NotificationsQuickSettingsContainer) {
156             mQSCustomizer.setContainer((NotificationsQuickSettingsContainer) container);
157         }
158     }
159 
isCustomizing()160     public boolean isCustomizing() {
161         return mQSCustomizer.isCustomizing();
162     }
163 
setHost(QSTileHost qsh)164     public void setHost(QSTileHost qsh) {
165         mQSPanel.setHost(qsh, mQSCustomizer);
166         mHeader.setQSPanel(mQSPanel);
167         mFooter.setQSPanel(mQSPanel);
168         mQSDetail.setHost(qsh);
169 
170         if (mQSAnimator != null) {
171             mQSAnimator.setHost(qsh);
172         }
173     }
174 
updateQsState()175     private void updateQsState() {
176         final boolean expandVisually = mQsExpanded || mStackScrollerOverscrolling
177                 || mHeaderAnimating;
178         mQSPanel.setExpanded(mQsExpanded);
179         mQSDetail.setExpanded(mQsExpanded);
180         mHeader.setVisibility((mQsExpanded || !mKeyguardShowing || mHeaderAnimating)
181                 ? View.VISIBLE
182                 : View.INVISIBLE);
183         mHeader.setExpanded((mKeyguardShowing && !mHeaderAnimating)
184                 || (mQsExpanded && !mStackScrollerOverscrolling));
185         mFooter.setVisibility((mQsExpanded || !mKeyguardShowing || mHeaderAnimating)
186                 ? View.VISIBLE
187                 : View.INVISIBLE);
188         mFooter.setExpanded((mKeyguardShowing && !mHeaderAnimating)
189                 || (mQsExpanded && !mStackScrollerOverscrolling));
190         mQSPanel.setVisibility(expandVisually ? View.VISIBLE : View.INVISIBLE);
191     }
192 
getQsPanel()193     public QSPanel getQsPanel() {
194         return mQSPanel;
195     }
196 
getCustomizer()197     public QSCustomizer getCustomizer() {
198         return mQSCustomizer;
199     }
200 
isShowingDetail()201     public boolean isShowingDetail() {
202         return mQSPanel.isShowingCustomize() || mQSDetail.isShowingDetail();
203     }
204 
setHeaderClickable(boolean clickable)205     public void setHeaderClickable(boolean clickable) {
206         if (DEBUG) Log.d(TAG, "setHeaderClickable " + clickable);
207         mFooter.getExpandView().setClickable(clickable);
208     }
209 
setExpanded(boolean expanded)210     public void setExpanded(boolean expanded) {
211         if (DEBUG) Log.d(TAG, "setExpanded " + expanded);
212         mQsExpanded = expanded;
213         mQSPanel.setListening(mListening && mQsExpanded);
214         updateQsState();
215     }
216 
setKeyguardShowing(boolean keyguardShowing)217     public void setKeyguardShowing(boolean keyguardShowing) {
218         if (DEBUG) Log.d(TAG, "setKeyguardShowing " + keyguardShowing);
219         mKeyguardShowing = keyguardShowing;
220 
221         if (mQSAnimator != null) {
222             mQSAnimator.setOnKeyguard(keyguardShowing);
223         }
224 
225         mFooter.setKeyguardShowing(keyguardShowing);
226         updateQsState();
227     }
228 
setOverscrolling(boolean stackScrollerOverscrolling)229     public void setOverscrolling(boolean stackScrollerOverscrolling) {
230         if (DEBUG) Log.d(TAG, "setOverscrolling " + stackScrollerOverscrolling);
231         mStackScrollerOverscrolling = stackScrollerOverscrolling;
232         updateQsState();
233     }
234 
setListening(boolean listening)235     public void setListening(boolean listening) {
236         if (DEBUG) Log.d(TAG, "setListening " + listening);
237         mListening = listening;
238         mHeader.setListening(listening);
239         mFooter.setListening(listening);
240         mQSPanel.setListening(mListening && mQsExpanded);
241     }
242 
setHeaderListening(boolean listening)243     public void setHeaderListening(boolean listening) {
244         mHeader.setListening(listening);
245         mFooter.setListening(listening);
246     }
247 
setQsExpansion(float expansion, float headerTranslation)248     public void setQsExpansion(float expansion, float headerTranslation) {
249         if (DEBUG) Log.d(TAG, "setQSExpansion " + expansion + " " + headerTranslation);
250         mContainer.setExpansion(expansion);
251         final float translationScaleY = expansion - 1;
252         if (!mHeaderAnimating) {
253             int height = mHeader.getHeight() + mGutterHeight;
254             getView().setTranslationY(mKeyguardShowing ? (translationScaleY * height)
255                     : headerTranslation);
256         }
257         mHeader.setExpansion(mKeyguardShowing ? 1 : expansion);
258         mFooter.setExpansion(mKeyguardShowing ? 1 : expansion);
259         int heightDiff = mQSPanel.getBottom() - mHeader.getBottom() + mHeader.getPaddingBottom()
260                 + mFooter.getHeight();
261         mQSPanel.setTranslationY(translationScaleY * heightDiff);
262         mQSDetail.setFullyExpanded(expansion == 1);
263 
264         if (mQSAnimator != null) {
265             mQSAnimator.setPosition(expansion);
266         }
267 
268         // Set bounds on the QS panel so it doesn't run over the header.
269         mQsBounds.top = (int) -mQSPanel.getTranslationY();
270         mQsBounds.right = mQSPanel.getWidth();
271         mQsBounds.bottom = mQSPanel.getHeight();
272         mQSPanel.setClipBounds(mQsBounds);
273     }
274 
animateHeaderSlidingIn(long delay)275     public void animateHeaderSlidingIn(long delay) {
276         if (DEBUG) Log.d(TAG, "animateHeaderSlidingIn");
277         // If the QS is already expanded we don't need to slide in the header as it's already
278         // visible.
279         if (!mQsExpanded) {
280             mHeaderAnimating = true;
281             mDelay = delay;
282             getView().getViewTreeObserver().addOnPreDrawListener(mStartHeaderSlidingIn);
283         }
284     }
285 
animateHeaderSlidingOut()286     public void animateHeaderSlidingOut() {
287         if (DEBUG) Log.d(TAG, "animateHeaderSlidingOut");
288         mHeaderAnimating = true;
289         getView().animate().y(-mHeader.getHeight())
290                 .setStartDelay(0)
291                 .setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD)
292                 .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
293                 .setListener(new AnimatorListenerAdapter() {
294                     @Override
295                     public void onAnimationEnd(Animator animation) {
296                         getView().animate().setListener(null);
297                         mHeaderAnimating = false;
298                         updateQsState();
299                     }
300                 })
301                 .start();
302     }
303 
304     @Override
setExpandClickListener(OnClickListener onClickListener)305     public void setExpandClickListener(OnClickListener onClickListener) {
306         mFooter.getExpandView().setOnClickListener(onClickListener);
307     }
308 
309     @Override
closeDetail()310     public void closeDetail() {
311         mQSPanel.closeDetail();
312     }
313 
notifyCustomizeChanged()314     public void notifyCustomizeChanged() {
315         // The customize state changed, so our height changed.
316         mContainer.updateExpansion();
317         mQSPanel.setVisibility(!mQSCustomizer.isCustomizing() ? View.VISIBLE : View.INVISIBLE);
318         mHeader.setVisibility(!mQSCustomizer.isCustomizing() ? View.VISIBLE : View.INVISIBLE);
319         mFooter.setVisibility(!mQSCustomizer.isCustomizing() ? View.VISIBLE : View.INVISIBLE);
320         // Let the panel know the position changed and it needs to update where notifications
321         // and whatnot are.
322         mPanelView.onQsHeightChanged();
323     }
324 
325     /**
326      * The height this view wants to be. This is different from {@link #getMeasuredHeight} such that
327      * during closing the detail panel, this already returns the smaller height.
328      */
getDesiredHeight()329     public int getDesiredHeight() {
330         if (mQSCustomizer.isCustomizing()) {
331             return getView().getHeight();
332         }
333         if (mQSDetail.isClosingDetail()) {
334             LayoutParams layoutParams = (LayoutParams) mQSPanel.getLayoutParams();
335             int panelHeight = layoutParams.topMargin + layoutParams.bottomMargin +
336                     + mQSPanel.getMeasuredHeight();
337             return panelHeight + getView().getPaddingBottom() + mGutterHeight;
338         } else {
339             return getView().getMeasuredHeight() + mGutterHeight;
340         }
341     }
342 
343     @Override
setHeightOverride(int desiredHeight)344     public void setHeightOverride(int desiredHeight) {
345         mContainer.setHeightOverride(desiredHeight - mGutterHeight);
346     }
347 
getQsMinExpansionHeight()348     public int getQsMinExpansionHeight() {
349         return mHeader.getHeight();
350     }
351 
352     @Override
hideImmediately()353     public void hideImmediately() {
354         getView().animate().cancel();
355         getView().setY(-mHeader.getHeight());
356     }
357 
358     private final ViewTreeObserver.OnPreDrawListener mStartHeaderSlidingIn
359             = new ViewTreeObserver.OnPreDrawListener() {
360         @Override
361         public boolean onPreDraw() {
362             getView().getViewTreeObserver().removeOnPreDrawListener(this);
363             getView().animate()
364                     .translationY(0f)
365                     .setStartDelay(mDelay)
366                     .setDuration(StackStateAnimator.ANIMATION_DURATION_GO_TO_FULL_SHADE)
367                     .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
368                     .setListener(mAnimateHeaderSlidingInListener)
369                     .start();
370             getView().setY(-mHeader.getHeight());
371             return true;
372         }
373     };
374 
375     private final Animator.AnimatorListener mAnimateHeaderSlidingInListener
376             = new AnimatorListenerAdapter() {
377         @Override
378         public void onAnimationEnd(Animator animation) {
379             mHeaderAnimating = false;
380             updateQsState();
381         }
382     };
383 }
384