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 com.android.internal.logging.nano.MetricsProto.MetricsEvent.ACTION_QS_MORE_SETTINGS;
18 
19 import android.animation.Animator;
20 import android.animation.Animator.AnimatorListener;
21 import android.animation.AnimatorListenerAdapter;
22 import android.annotation.Nullable;
23 import android.content.Context;
24 import android.content.Intent;
25 import android.content.res.Configuration;
26 import android.content.res.Resources;
27 import android.graphics.drawable.Animatable;
28 import android.util.AttributeSet;
29 import android.util.SparseArray;
30 import android.view.View;
31 import android.view.ViewGroup;
32 import android.view.ViewStub;
33 import android.view.WindowInsets;
34 import android.view.accessibility.AccessibilityEvent;
35 import android.widget.ImageView;
36 import android.widget.LinearLayout;
37 import android.widget.Switch;
38 import android.widget.TextView;
39 
40 import com.android.internal.annotations.VisibleForTesting;
41 import com.android.internal.logging.MetricsLogger;
42 import com.android.internal.logging.UiEventLogger;
43 import com.android.internal.policy.SystemBarUtils;
44 import com.android.systemui.Dependency;
45 import com.android.systemui.FontSizeUtils;
46 import com.android.systemui.R;
47 import com.android.systemui.plugins.ActivityStarter;
48 import com.android.systemui.plugins.FalsingManager;
49 import com.android.systemui.plugins.qs.DetailAdapter;
50 import com.android.systemui.plugins.qs.QSContainerController;
51 import com.android.systemui.statusbar.CommandQueue;
52 
53 public class QSDetail extends LinearLayout {
54 
55     private static final String TAG = "QSDetail";
56     private static final long FADE_DURATION = 300;
57 
58     private final SparseArray<View> mDetailViews = new SparseArray<>();
59     private final UiEventLogger mUiEventLogger = QSEvents.INSTANCE.getQsUiEventsLogger();
60 
61     private ViewGroup mDetailContent;
62     private FalsingManager mFalsingManager;
63     protected TextView mDetailSettingsButton;
64     protected TextView mDetailDoneButton;
65     @VisibleForTesting
66     QSDetailClipper mClipper;
67     private DetailAdapter mDetailAdapter;
68     private QSPanelController mQsPanelController;
69 
70     protected View mQsDetailHeader;
71     protected TextView mQsDetailHeaderTitle;
72     private ViewStub mQsDetailHeaderSwitchStub;
73     private Switch mQsDetailHeaderSwitch;
74     protected ImageView mQsDetailHeaderProgress;
75 
76     protected QSTileHost mHost;
77 
78     private boolean mScanState;
79     private boolean mClosingDetail;
80     private boolean mFullyExpanded;
81     private QuickStatusBarHeader mHeader;
82     private boolean mTriggeredExpand;
83     private boolean mShouldAnimate;
84     private int mOpenX;
85     private int mOpenY;
86     private boolean mAnimatingOpen;
87     private boolean mSwitchState;
88     private QSFooter mFooter;
89 
90     private QSContainerController mQsContainerController;
91 
QSDetail(Context context, @Nullable AttributeSet attrs)92     public QSDetail(Context context, @Nullable AttributeSet attrs) {
93         super(context, attrs);
94     }
95 
96     @Override
onConfigurationChanged(Configuration newConfig)97     protected void onConfigurationChanged(Configuration newConfig) {
98         super.onConfigurationChanged(newConfig);
99         FontSizeUtils.updateFontSize(mDetailDoneButton, R.dimen.qs_detail_button_text_size);
100         FontSizeUtils.updateFontSize(mDetailSettingsButton, R.dimen.qs_detail_button_text_size);
101 
102         for (int i = 0; i < mDetailViews.size(); i++) {
103             mDetailViews.valueAt(i).dispatchConfigurationChanged(newConfig);
104         }
105     }
106 
107     @Override
onFinishInflate()108     protected void onFinishInflate() {
109         super.onFinishInflate();
110         mDetailContent = findViewById(android.R.id.content);
111         mDetailSettingsButton = findViewById(android.R.id.button2);
112         mDetailDoneButton = findViewById(android.R.id.button1);
113 
114         mQsDetailHeader = findViewById(R.id.qs_detail_header);
115         mQsDetailHeaderTitle = (TextView) mQsDetailHeader.findViewById(android.R.id.title);
116         mQsDetailHeaderSwitchStub = mQsDetailHeader.findViewById(R.id.toggle_stub);
117         mQsDetailHeaderProgress = findViewById(R.id.qs_detail_header_progress);
118 
119         updateDetailText();
120 
121         mClipper = new QSDetailClipper(this);
122     }
123 
setContainerController(QSContainerController controller)124     public void setContainerController(QSContainerController controller) {
125         mQsContainerController = controller;
126     }
127 
128     /** */
setQsPanel(QSPanelController panelController, QuickStatusBarHeader header, QSFooter footer, FalsingManager falsingManager)129     public void setQsPanel(QSPanelController panelController, QuickStatusBarHeader header,
130             QSFooter footer, FalsingManager falsingManager) {
131         mQsPanelController = panelController;
132         mHeader = header;
133         mFooter = footer;
134         mHeader.setCallback(mQsPanelCallback);
135         mQsPanelController.setCallback(mQsPanelCallback);
136         mFalsingManager = falsingManager;
137     }
138 
setHost(QSTileHost host)139     public void setHost(QSTileHost host) {
140         mHost = host;
141     }
isShowingDetail()142     public boolean isShowingDetail() {
143         return mDetailAdapter != null;
144     }
145 
setFullyExpanded(boolean fullyExpanded)146     public void setFullyExpanded(boolean fullyExpanded) {
147         mFullyExpanded = fullyExpanded;
148     }
149 
setExpanded(boolean qsExpanded)150     public void setExpanded(boolean qsExpanded) {
151         if (!qsExpanded) {
152             mTriggeredExpand = false;
153         }
154     }
155 
updateDetailText()156     private void updateDetailText() {
157         int resId = mDetailAdapter != null ? mDetailAdapter.getDoneText() : Resources.ID_NULL;
158         mDetailDoneButton.setText(
159                 (resId != Resources.ID_NULL) ? resId : R.string.quick_settings_done);
160         resId = mDetailAdapter != null ? mDetailAdapter.getSettingsText() : Resources.ID_NULL;
161         mDetailSettingsButton.setText(
162                 (resId != Resources.ID_NULL) ? resId : R.string.quick_settings_more_settings);
163     }
164 
updateResources()165     public void updateResources() {
166         updateDetailText();
167         MarginLayoutParams lp = (MarginLayoutParams) getLayoutParams();
168         lp.topMargin = SystemBarUtils.getQuickQsOffsetHeight(mContext);
169         setLayoutParams(lp);
170     }
171 
172     @Override
onApplyWindowInsets(WindowInsets insets)173     public WindowInsets onApplyWindowInsets(WindowInsets insets) {
174         int bottomNavBar = insets.getInsets(WindowInsets.Type.navigationBars()).bottom;
175         MarginLayoutParams lp = (MarginLayoutParams) getLayoutParams();
176         lp.bottomMargin = bottomNavBar;
177         setLayoutParams(lp);
178         return super.onApplyWindowInsets(insets);
179     }
180 
isClosingDetail()181     public boolean isClosingDetail() {
182         return mClosingDetail;
183     }
184 
185     public interface Callback {
onShowingDetail(DetailAdapter detail, int x, int y)186         void onShowingDetail(DetailAdapter detail, int x, int y);
onToggleStateChanged(boolean state)187         void onToggleStateChanged(boolean state);
onScanStateChanged(boolean state)188         void onScanStateChanged(boolean state);
189     }
190 
handleShowingDetail(final DetailAdapter adapter, int x, int y, boolean toggleQs)191     public void handleShowingDetail(final DetailAdapter adapter, int x, int y,
192             boolean toggleQs) {
193         final boolean showingDetail = adapter != null;
194         final boolean wasShowingDetail = mDetailAdapter != null;
195         setClickable(showingDetail);
196         if (showingDetail) {
197             setupDetailHeader(adapter);
198             if (toggleQs && !mFullyExpanded) {
199                 mTriggeredExpand = true;
200                 Dependency.get(CommandQueue.class).animateExpandSettingsPanel(null);
201             } else {
202                 mTriggeredExpand = false;
203             }
204             mShouldAnimate = adapter.shouldAnimate();
205             mOpenX = x;
206             mOpenY = y;
207         } else {
208             // Ensure we collapse into the same point we opened from.
209             x = mOpenX;
210             y = mOpenY;
211             if (toggleQs && mTriggeredExpand) {
212                 Dependency.get(CommandQueue.class).animateCollapsePanels();
213                 mTriggeredExpand = false;
214             }
215             // Always animate on close, even if the last opened detail adapter had shouldAnimate()
216             // return false. This is necessary to avoid a race condition which could leave the
217             // keyguard in a bad state where QS remains visible underneath the notifications, clock,
218             // and status area.
219             mShouldAnimate = true;
220         }
221 
222         boolean visibleDiff = wasShowingDetail != showingDetail;
223         if (!visibleDiff && !wasShowingDetail) return;  // already in right state
224         AnimatorListener listener;
225         if (showingDetail) {
226             int viewCacheIndex = adapter.getMetricsCategory();
227             View detailView = adapter.createDetailView(mContext, mDetailViews.get(viewCacheIndex),
228                     mDetailContent);
229             if (detailView == null) throw new IllegalStateException("Must return detail view");
230 
231             setupDetailFooter(adapter);
232 
233             mDetailContent.removeAllViews();
234             mDetailContent.addView(detailView);
235             mDetailViews.put(viewCacheIndex, detailView);
236             Dependency.get(MetricsLogger.class).visible(adapter.getMetricsCategory());
237             mUiEventLogger.log(adapter.openDetailEvent());
238             announceForAccessibility(mContext.getString(
239                     R.string.accessibility_quick_settings_detail,
240                     adapter.getTitle()));
241             mDetailAdapter = adapter;
242             listener = mHideGridContentWhenDone;
243             setVisibility(View.VISIBLE);
244             updateDetailText();
245         } else {
246             if (wasShowingDetail) {
247                 Dependency.get(MetricsLogger.class).hidden(mDetailAdapter.getMetricsCategory());
248                 mUiEventLogger.log(mDetailAdapter.closeDetailEvent());
249             }
250             mClosingDetail = true;
251             mDetailAdapter = null;
252             listener = mTeardownDetailWhenDone;
253             // Only update visibility if already expanded. Otherwise, a race condition can cause the
254             // keyguard to enter a bad state where the QS tiles are displayed underneath the
255             // notifications, clock, and status area.
256             if (mQsPanelController.isExpanded()) {
257                 mHeader.setVisibility(View.VISIBLE);
258                 mFooter.setVisibility(View.VISIBLE);
259                 mQsPanelController.setGridContentVisibility(true);
260                 mQsPanelCallback.onScanStateChanged(false);
261             }
262         }
263         sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
264         animateDetailVisibleDiff(x, y, visibleDiff, listener);
265         if (mQsContainerController != null) {
266             mQsContainerController.setDetailShowing(showingDetail);
267         }
268     }
269 
animateDetailVisibleDiff(int x, int y, boolean visibleDiff, AnimatorListener listener)270     protected void animateDetailVisibleDiff(int x, int y, boolean visibleDiff, AnimatorListener listener) {
271         if (visibleDiff) {
272             mAnimatingOpen = mDetailAdapter != null;
273             if (mFullyExpanded || mDetailAdapter != null) {
274                 setAlpha(1);
275                 mClipper.updateCircularClip(mShouldAnimate, x, y, mDetailAdapter != null, listener);
276             } else {
277                 animate().alpha(0)
278                         .setDuration(mShouldAnimate ? FADE_DURATION : 0)
279                         .setListener(listener)
280                         .start();
281             }
282         }
283     }
284 
setupDetailFooter(DetailAdapter adapter)285     protected void setupDetailFooter(DetailAdapter adapter) {
286         final Intent settingsIntent = adapter.getSettingsIntent();
287         mDetailSettingsButton.setVisibility(settingsIntent != null ? VISIBLE : GONE);
288         mDetailSettingsButton.setOnClickListener(v -> {
289             if (mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
290                 return;
291             }
292             Dependency.get(MetricsLogger.class).action(ACTION_QS_MORE_SETTINGS,
293                     adapter.getMetricsCategory());
294             mUiEventLogger.log(adapter.moreSettingsEvent());
295             Dependency.get(ActivityStarter.class)
296                     .postStartActivityDismissingKeyguard(settingsIntent, 0);
297         });
298         mDetailDoneButton.setOnClickListener(v -> {
299             if (mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
300                 return;
301             }
302             announceForAccessibility(
303                     mContext.getString(R.string.accessibility_desc_quick_settings));
304             if (!adapter.onDoneButtonClicked()) {
305                 mQsPanelController.closeDetail();
306             }
307         });
308     }
309 
setupDetailHeader(final DetailAdapter adapter)310     protected void setupDetailHeader(final DetailAdapter adapter) {
311         mQsDetailHeaderTitle.setText(adapter.getTitle());
312         final Boolean toggleState = adapter.getToggleState();
313         if (toggleState == null) {
314             if (mQsDetailHeaderSwitch != null) mQsDetailHeaderSwitch.setVisibility(INVISIBLE);
315             mQsDetailHeader.setClickable(false);
316         } else {
317             if (mQsDetailHeaderSwitch == null) {
318                 mQsDetailHeaderSwitch = (Switch) mQsDetailHeaderSwitchStub.inflate();
319             }
320             mQsDetailHeaderSwitch.setVisibility(VISIBLE);
321             handleToggleStateChanged(toggleState, adapter.getToggleEnabled());
322             mQsDetailHeader.setClickable(true);
323             mQsDetailHeader.setOnClickListener(v -> {
324                 if (mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
325                     return;
326                 }
327                 boolean checked = !mQsDetailHeaderSwitch.isChecked();
328                 mQsDetailHeaderSwitch.setChecked(checked);
329                 adapter.setToggleState(checked);
330             });
331         }
332     }
333 
handleToggleStateChanged(boolean state, boolean toggleEnabled)334     private void handleToggleStateChanged(boolean state, boolean toggleEnabled) {
335         mSwitchState = state;
336         if (mAnimatingOpen) {
337             return;
338         }
339         if (mQsDetailHeaderSwitch != null) mQsDetailHeaderSwitch.setChecked(state);
340         mQsDetailHeader.setEnabled(toggleEnabled);
341         if (mQsDetailHeaderSwitch != null) mQsDetailHeaderSwitch.setEnabled(toggleEnabled);
342     }
343 
handleScanStateChanged(boolean state)344     private void handleScanStateChanged(boolean state) {
345         if (mScanState == state) return;
346         mScanState = state;
347         final Animatable anim = (Animatable) mQsDetailHeaderProgress.getDrawable();
348         if (state) {
349             mQsDetailHeaderProgress.animate().cancel();
350             mQsDetailHeaderProgress.animate()
351                     .alpha(1)
352                     .withEndAction(anim::start)
353                     .start();
354         } else {
355             mQsDetailHeaderProgress.animate().cancel();
356             mQsDetailHeaderProgress.animate()
357                     .alpha(0f)
358                     .withEndAction(anim::stop)
359                     .start();
360         }
361     }
362 
checkPendingAnimations()363     private void checkPendingAnimations() {
364         handleToggleStateChanged(mSwitchState,
365                             mDetailAdapter != null && mDetailAdapter.getToggleEnabled());
366     }
367 
368     protected Callback mQsPanelCallback = new Callback() {
369         @Override
370         public void onToggleStateChanged(final boolean state) {
371             post(new Runnable() {
372                 @Override
373                 public void run() {
374                     handleToggleStateChanged(state,
375                             mDetailAdapter != null && mDetailAdapter.getToggleEnabled());
376                 }
377             });
378         }
379 
380         @Override
381         public void onShowingDetail(final DetailAdapter detail, final int x, final int y) {
382             post(new Runnable() {
383                 @Override
384                 public void run() {
385                     if (isAttachedToWindow()) {
386                         handleShowingDetail(detail, x, y, false /* toggleQs */);
387                     }
388                 }
389             });
390         }
391 
392         @Override
393         public void onScanStateChanged(final boolean state) {
394             post(new Runnable() {
395                 @Override
396                 public void run() {
397                     handleScanStateChanged(state);
398                 }
399             });
400         }
401     };
402 
403     private final AnimatorListenerAdapter mHideGridContentWhenDone = new AnimatorListenerAdapter() {
404         public void onAnimationCancel(Animator animation) {
405             // If we have been cancelled, remove the listener so that onAnimationEnd doesn't get
406             // called, this will avoid accidentally turning off the grid when we don't want to.
407             animation.removeListener(this);
408             mAnimatingOpen = false;
409             checkPendingAnimations();
410         };
411 
412         @Override
413         public void onAnimationEnd(Animator animation) {
414             // Only hide content if still in detail state.
415             if (mDetailAdapter != null) {
416                 mQsPanelController.setGridContentVisibility(false);
417                 mHeader.setVisibility(View.INVISIBLE);
418                 mFooter.setVisibility(View.INVISIBLE);
419             }
420             mAnimatingOpen = false;
421             checkPendingAnimations();
422         }
423     };
424 
425     private final AnimatorListenerAdapter mTeardownDetailWhenDone = new AnimatorListenerAdapter() {
426         public void onAnimationEnd(Animator animation) {
427             mDetailContent.removeAllViews();
428             setVisibility(View.INVISIBLE);
429             mClosingDetail = false;
430         };
431     };
432 }
433