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