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
QSFragment(RemoteInputQuickSettingsDisabler remoteInputQsDisabler, InjectionInflationController injectionInflater, Context context, QSTileHost qsTileHost)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
onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState)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
onViewCreated(View view, @Nullable Bundle savedInstanceState)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
onDestroy()132     public void onDestroy() {
133         super.onDestroy();
134         if (mListening) {
135             setListening(false);
136         }
137     }
138 
139     @Override
onSaveInstanceState(Bundle outState)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
isListening()151     boolean isListening() {
152         return mListening;
153     }
154 
155     @VisibleForTesting
isExpanded()156     boolean isExpanded() {
157         return mQsExpanded;
158     }
159 
160     @Override
getHeader()161     public View getHeader() {
162         return mHeader;
163     }
164 
165     @Override
setHasNotifications(boolean hasNotifications)166     public void setHasNotifications(boolean hasNotifications) {
167     }
168 
169     @Override
setPanelView(HeightListener panelView)170     public void setPanelView(HeightListener panelView) {
171         mPanelView = panelView;
172     }
173 
174     @Override
onConfigurationChanged(Configuration newConfig)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 
setEditLocation(View view)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
setContainer(ViewGroup container)195     public void setContainer(ViewGroup container) {
196         if (container instanceof NotificationsQuickSettingsContainer) {
197             mQSCustomizer.setContainer((NotificationsQuickSettingsContainer) container);
198         }
199     }
200 
201     @Override
isCustomizing()202     public boolean isCustomizing() {
203         return mQSCustomizer.isCustomizing();
204     }
205 
setHost(QSTileHost qsh)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
disable(int displayId, int state1, int state2, boolean animate)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 
updateQsState()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 
getQsPanel()252     public QSPanel getQsPanel() {
253         return mQSPanel;
254     }
255 
getCustomizer()256     public QSCustomizer getCustomizer() {
257         return mQSCustomizer;
258     }
259 
260     @Override
isShowingDetail()261     public boolean isShowingDetail() {
262         return mQSPanel.isShowingCustomize() || mQSDetail.isShowingDetail();
263     }
264 
265     @Override
onInterceptTouchEvent(MotionEvent event)266     public boolean onInterceptTouchEvent(MotionEvent event) {
267         return isCustomizing();
268     }
269 
270     @Override
setHeaderClickable(boolean clickable)271     public void setHeaderClickable(boolean clickable) {
272         if (DEBUG) Log.d(TAG, "setHeaderClickable " + clickable);
273     }
274 
275     @Override
setExpanded(boolean expanded)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
setKeyguardShowing(boolean keyguardShowing)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
setOverscrolling(boolean stackScrollerOverscrolling)298     public void setOverscrolling(boolean stackScrollerOverscrolling) {
299         if (DEBUG) Log.d(TAG, "setOverscrolling " + stackScrollerOverscrolling);
300         mStackScrollerOverscrolling = stackScrollerOverscrolling;
301         updateQsState();
302     }
303 
304     @Override
setListening(boolean listening)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
setHeaderListening(boolean listening)314     public void setHeaderListening(boolean listening) {
315         mHeader.setListening(listening);
316         mFooter.setListening(listening);
317     }
318 
319     @Override
setQsExpansion(float expansion, float headerTranslation)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
animateHeaderSlidingIn(long delay)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
animateHeaderSlidingOut()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
setExpandClickListener(OnClickListener onClickListener)400     public void setExpandClickListener(OnClickListener onClickListener) {
401         mFooter.setExpandClickListener(onClickListener);
402     }
403 
404     @Override
closeDetail()405     public void closeDetail() {
406         mQSPanel.closeDetail();
407     }
408 
notifyCustomizeChanged()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
getDesiredHeight()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
setHeightOverride(int desiredHeight)439     public void setHeightOverride(int desiredHeight) {
440         mContainer.setHeightOverride(desiredHeight);
441     }
442 
443     @Override
getQsMinExpansionHeight()444     public int getQsMinExpansionHeight() {
445         return mHeader.getHeight();
446     }
447 
448     @Override
hideImmediately()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