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