blob: ee3b499edfb7bf85962f1ff5ef29b3484bed3da5 [file] [log] [blame]
Jason Monk377e7ad2016-02-16 14:03:21 -05001/*
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
15package com.android.systemui.qs;
16
Jason Monk8c09ac72017-03-16 11:53:40 -040017import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.ACTION_QS_MORE_SETTINGS;
18
Jason Monk377e7ad2016-02-16 14:03:21 -050019import android.animation.Animator;
20import android.animation.Animator.AnimatorListener;
21import android.animation.AnimatorListenerAdapter;
22import android.annotation.Nullable;
23import android.content.Context;
24import android.content.Intent;
25import android.content.res.Configuration;
26import android.graphics.drawable.Animatable;
27import android.util.AttributeSet;
Fabian Kozynskidcd3b3b2020-02-18 15:05:04 -050028import android.util.Pair;
Jason Monk377e7ad2016-02-16 14:03:21 -050029import android.util.SparseArray;
Fabian Kozynskidcd3b3b2020-02-18 15:05:04 -050030import android.view.DisplayCutout;
Jason Monk377e7ad2016-02-16 14:03:21 -050031import android.view.View;
32import android.view.ViewGroup;
Fabian Kozynski8a867952019-12-02 10:32:00 -050033import android.view.ViewStub;
Fabian Kozynskidcd3b3b2020-02-18 15:05:04 -050034import android.view.WindowInsets;
Jason Monk377e7ad2016-02-16 14:03:21 -050035import android.view.accessibility.AccessibilityEvent;
36import android.widget.ImageView;
37import android.widget.LinearLayout;
38import android.widget.Switch;
39import android.widget.TextView;
Xiaohui Chen66448932016-04-18 12:53:28 -070040
Jason Monk377e7ad2016-02-16 14:03:21 -050041import com.android.internal.logging.MetricsLogger;
Fabian Kozynski2ff6df92020-04-24 12:00:49 -040042import com.android.internal.logging.UiEventLogger;
Jason Monk9c7844c2017-01-18 15:21:53 -050043import com.android.systemui.Dependency;
Jason Monk377e7ad2016-02-16 14:03:21 -050044import com.android.systemui.FontSizeUtils;
45import com.android.systemui.R;
Jason Monkec34da82017-02-24 15:57:05 -050046import com.android.systemui.plugins.ActivityStarter;
Jason Monke5b770e2017-03-03 21:49:29 -050047import com.android.systemui.plugins.qs.DetailAdapter;
Jason Monk9c7844c2017-01-18 15:21:53 -050048import com.android.systemui.statusbar.CommandQueue;
Fabian Kozynskidcd3b3b2020-02-18 15:05:04 -050049import com.android.systemui.statusbar.phone.StatusBarWindowView;
Jason Monk377e7ad2016-02-16 14:03:21 -050050
51public class QSDetail extends LinearLayout {
52
53 private static final String TAG = "QSDetail";
54 private static final long FADE_DURATION = 300;
55
56 private final SparseArray<View> mDetailViews = new SparseArray<>();
Fabian Kozynski2ff6df92020-04-24 12:00:49 -040057 private final UiEventLogger mUiEventLogger = QSEvents.INSTANCE.getQsUiEventsLogger();
Jason Monk377e7ad2016-02-16 14:03:21 -050058
59 private ViewGroup mDetailContent;
Muyuan Li0e9f5382016-04-27 15:51:15 -070060 protected TextView mDetailSettingsButton;
61 protected TextView mDetailDoneButton;
Jason Monk377e7ad2016-02-16 14:03:21 -050062 private QSDetailClipper mClipper;
63 private DetailAdapter mDetailAdapter;
64 private QSPanel mQsPanel;
65
Muyuan Li0e9f5382016-04-27 15:51:15 -070066 protected View mQsDetailHeader;
67 protected TextView mQsDetailHeaderTitle;
Fabian Kozynski8a867952019-12-02 10:32:00 -050068 private ViewStub mQsDetailHeaderSwitchStub;
69 private Switch mQsDetailHeaderSwitch;
Muyuan Lie3cecd72016-05-14 12:35:12 -070070 protected ImageView mQsDetailHeaderProgress;
Jason Monk377e7ad2016-02-16 14:03:21 -050071
Muyuan Li0e9f5382016-04-27 15:51:15 -070072 protected QSTileHost mHost;
Jason Monk377e7ad2016-02-16 14:03:21 -050073
74 private boolean mScanState;
75 private boolean mClosingDetail;
76 private boolean mFullyExpanded;
Jason Monke5b770e2017-03-03 21:49:29 -050077 private QuickStatusBarHeader mHeader;
Xiaohui Chen66448932016-04-18 12:53:28 -070078 private boolean mTriggeredExpand;
79 private int mOpenX;
80 private int mOpenY;
Jason Monk35a9c142016-07-25 10:18:30 -040081 private boolean mAnimatingOpen;
Jason Monk48e093e2016-07-15 10:37:07 -040082 private boolean mSwitchState;
Jason Monkb4ec0b92017-06-13 13:47:54 -040083 private View mFooter;
Jason Monk377e7ad2016-02-16 14:03:21 -050084
85 public QSDetail(Context context, @Nullable AttributeSet attrs) {
86 super(context, attrs);
87 }
88
89 @Override
90 protected void onConfigurationChanged(Configuration newConfig) {
91 super.onConfigurationChanged(newConfig);
92 FontSizeUtils.updateFontSize(mDetailDoneButton, R.dimen.qs_detail_button_text_size);
93 FontSizeUtils.updateFontSize(mDetailSettingsButton, R.dimen.qs_detail_button_text_size);
94
95 for (int i = 0; i < mDetailViews.size(); i++) {
96 mDetailViews.valueAt(i).dispatchConfigurationChanged(newConfig);
97 }
98 }
99
100 @Override
101 protected void onFinishInflate() {
102 super.onFinishInflate();
Alan Viverette51efddb2017-04-05 10:00:01 -0400103 mDetailContent = findViewById(android.R.id.content);
104 mDetailSettingsButton = findViewById(android.R.id.button2);
105 mDetailDoneButton = findViewById(android.R.id.button1);
Jason Monk377e7ad2016-02-16 14:03:21 -0500106
107 mQsDetailHeader = findViewById(R.id.qs_detail_header);
Jason Monk377e7ad2016-02-16 14:03:21 -0500108 mQsDetailHeaderTitle = (TextView) mQsDetailHeader.findViewById(android.R.id.title);
Fabian Kozynski8a867952019-12-02 10:32:00 -0500109 mQsDetailHeaderSwitchStub = mQsDetailHeader.findViewById(R.id.toggle_stub);
Alan Viverette51efddb2017-04-05 10:00:01 -0400110 mQsDetailHeaderProgress = findViewById(R.id.qs_detail_header_progress);
Jason Monk377e7ad2016-02-16 14:03:21 -0500111
112 updateDetailText();
113
114 mClipper = new QSDetailClipper(this);
115
116 final OnClickListener doneListener = new OnClickListener() {
117 @Override
118 public void onClick(View v) {
119 announceForAccessibility(
120 mContext.getString(R.string.accessibility_desc_quick_settings));
121 mQsPanel.closeDetail();
122 }
123 };
Jason Monk377e7ad2016-02-16 14:03:21 -0500124 mDetailDoneButton.setOnClickListener(doneListener);
125 }
126
Jason Monkb4ec0b92017-06-13 13:47:54 -0400127 public void setQsPanel(QSPanel panel, QuickStatusBarHeader header, View footer) {
Jason Monk377e7ad2016-02-16 14:03:21 -0500128 mQsPanel = panel;
Jason Monk589bb702016-04-12 15:44:43 -0400129 mHeader = header;
Jason Monkb4ec0b92017-06-13 13:47:54 -0400130 mFooter = footer;
Xiaohui Chen66448932016-04-18 12:53:28 -0700131 mHeader.setCallback(mQsPanelCallback);
Jason Monk377e7ad2016-02-16 14:03:21 -0500132 mQsPanel.setCallback(mQsPanelCallback);
133 }
134
135 public void setHost(QSTileHost host) {
136 mHost = host;
137 }
138 public boolean isShowingDetail() {
139 return mDetailAdapter != null;
140 }
141
142 public void setFullyExpanded(boolean fullyExpanded) {
143 mFullyExpanded = fullyExpanded;
144 }
145
Xiaohui Chen66448932016-04-18 12:53:28 -0700146 public void setExpanded(boolean qsExpanded) {
147 if (!qsExpanded) {
148 mTriggeredExpand = false;
149 }
150 }
151
Jason Monk377e7ad2016-02-16 14:03:21 -0500152 private void updateDetailText() {
153 mDetailDoneButton.setText(R.string.quick_settings_done);
154 mDetailSettingsButton.setText(R.string.quick_settings_more_settings);
155 }
156
157 public void updateResources() {
158 updateDetailText();
159 }
160
161 public boolean isClosingDetail() {
162 return mClosingDetail;
163 }
164
Jason Monke5b770e2017-03-03 21:49:29 -0500165 public interface Callback {
166 void onShowingDetail(DetailAdapter detail, int x, int y);
167 void onToggleStateChanged(boolean state);
168 void onScanStateChanged(boolean state);
169 }
Muyuan Li0e9f5382016-04-27 15:51:15 -0700170
Jason Monk46767b72016-08-18 10:58:04 -0400171 public void handleShowingDetail(final DetailAdapter adapter, int x, int y,
Muyuan Libc55b2a2016-06-24 15:40:51 -0700172 boolean toggleQs) {
Jason Monk377e7ad2016-02-16 14:03:21 -0500173 final boolean showingDetail = adapter != null;
174 setClickable(showingDetail);
175 if (showingDetail) {
Muyuan Li0e9f5382016-04-27 15:51:15 -0700176 setupDetailHeader(adapter);
Muyuan Libc55b2a2016-06-24 15:40:51 -0700177 if (toggleQs && !mFullyExpanded) {
Xiaohui Chen66448932016-04-18 12:53:28 -0700178 mTriggeredExpand = true;
Dave Mankoffbcaca8a2019-10-31 18:04:08 -0400179 Dependency.get(CommandQueue.class).animateExpandSettingsPanel(null);
Xiaohui Chen66448932016-04-18 12:53:28 -0700180 } else {
181 mTriggeredExpand = false;
182 }
183 mOpenX = x;
184 mOpenY = y;
185 } else {
186 // Ensure we collapse into the same point we opened from.
187 x = mOpenX;
188 y = mOpenY;
Muyuan Libc55b2a2016-06-24 15:40:51 -0700189 if (toggleQs && mTriggeredExpand) {
Dave Mankoffbcaca8a2019-10-31 18:04:08 -0400190 Dependency.get(CommandQueue.class).animateCollapsePanels();
Xiaohui Chen66448932016-04-18 12:53:28 -0700191 mTriggeredExpand = false;
192 }
Jason Monk377e7ad2016-02-16 14:03:21 -0500193 }
194
195 boolean visibleDiff = (mDetailAdapter != null) != (adapter != null);
196 if (!visibleDiff && mDetailAdapter == adapter) return; // already in right state
197 AnimatorListener listener = null;
198 if (adapter != null) {
199 int viewCacheIndex = adapter.getMetricsCategory();
200 View detailView = adapter.createDetailView(mContext, mDetailViews.get(viewCacheIndex),
201 mDetailContent);
202 if (detailView == null) throw new IllegalStateException("Must return detail view");
203
Muyuan Li0e9f5382016-04-27 15:51:15 -0700204 setupDetailFooter(adapter);
Jason Monk377e7ad2016-02-16 14:03:21 -0500205
206 mDetailContent.removeAllViews();
207 mDetailContent.addView(detailView);
208 mDetailViews.put(viewCacheIndex, detailView);
Jason Monk8c09ac72017-03-16 11:53:40 -0400209 Dependency.get(MetricsLogger.class).visible(adapter.getMetricsCategory());
Fabian Kozynski2ff6df92020-04-24 12:00:49 -0400210 mUiEventLogger.log(adapter.openDetailEvent());
Jason Monk377e7ad2016-02-16 14:03:21 -0500211 announceForAccessibility(mContext.getString(
212 R.string.accessibility_quick_settings_detail,
213 adapter.getTitle()));
214 mDetailAdapter = adapter;
215 listener = mHideGridContentWhenDone;
216 setVisibility(View.VISIBLE);
217 } else {
218 if (mDetailAdapter != null) {
Jason Monk8c09ac72017-03-16 11:53:40 -0400219 Dependency.get(MetricsLogger.class).hidden(mDetailAdapter.getMetricsCategory());
Fabian Kozynski2ff6df92020-04-24 12:00:49 -0400220 mUiEventLogger.log(mDetailAdapter.closeDetailEvent());
Jason Monk377e7ad2016-02-16 14:03:21 -0500221 }
222 mClosingDetail = true;
223 mDetailAdapter = null;
224 listener = mTeardownDetailWhenDone;
Jason Monk589bb702016-04-12 15:44:43 -0400225 mHeader.setVisibility(View.VISIBLE);
Jason Monkb4ec0b92017-06-13 13:47:54 -0400226 mFooter.setVisibility(View.VISIBLE);
Jason Monk377e7ad2016-02-16 14:03:21 -0500227 mQsPanel.setGridContentVisibility(true);
228 mQsPanelCallback.onScanStateChanged(false);
229 }
230 sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
Muyuan Li0e9f5382016-04-27 15:51:15 -0700231
232 animateDetailVisibleDiff(x, y, visibleDiff, listener);
233 }
234
235 protected void animateDetailVisibleDiff(int x, int y, boolean visibleDiff, AnimatorListener listener) {
Jason Monk377e7ad2016-02-16 14:03:21 -0500236 if (visibleDiff) {
Jason Monkac3dec92016-07-26 09:22:18 -0400237 mAnimatingOpen = mDetailAdapter != null;
Jason Monk377e7ad2016-02-16 14:03:21 -0500238 if (mFullyExpanded || mDetailAdapter != null) {
239 setAlpha(1);
240 mClipper.animateCircularClip(x, y, mDetailAdapter != null, listener);
241 } else {
242 animate().alpha(0)
243 .setDuration(FADE_DURATION)
244 .setListener(listener)
245 .start();
246 }
247 }
248 }
249
Muyuan Li0e9f5382016-04-27 15:51:15 -0700250 protected void setupDetailFooter(DetailAdapter adapter) {
251 final Intent settingsIntent = adapter.getSettingsIntent();
252 mDetailSettingsButton.setVisibility(settingsIntent != null ? VISIBLE : GONE);
Jason Monk8c09ac72017-03-16 11:53:40 -0400253 mDetailSettingsButton.setOnClickListener(v -> {
254 Dependency.get(MetricsLogger.class).action(ACTION_QS_MORE_SETTINGS,
Jason Monk6997da72017-04-03 15:22:54 -0400255 adapter.getMetricsCategory());
Fabian Kozynski2ff6df92020-04-24 12:00:49 -0400256 mUiEventLogger.log(adapter.moreSettingsEvent());
Jason Monk8c09ac72017-03-16 11:53:40 -0400257 Dependency.get(ActivityStarter.class)
258 .postStartActivityDismissingKeyguard(settingsIntent, 0);
259 });
Muyuan Li0e9f5382016-04-27 15:51:15 -0700260 }
261
262 protected void setupDetailHeader(final DetailAdapter adapter) {
263 mQsDetailHeaderTitle.setText(adapter.getTitle());
264 final Boolean toggleState = adapter.getToggleState();
265 if (toggleState == null) {
Fabian Kozynski8a867952019-12-02 10:32:00 -0500266 if (mQsDetailHeaderSwitch != null) mQsDetailHeaderSwitch.setVisibility(INVISIBLE);
Muyuan Li0e9f5382016-04-27 15:51:15 -0700267 mQsDetailHeader.setClickable(false);
268 } else {
Fabian Kozynski8a867952019-12-02 10:32:00 -0500269 if (mQsDetailHeaderSwitch == null) {
270 mQsDetailHeaderSwitch = (Switch) mQsDetailHeaderSwitchStub.inflate();
271 }
Muyuan Li0e9f5382016-04-27 15:51:15 -0700272 mQsDetailHeaderSwitch.setVisibility(VISIBLE);
Jason Monkac3dec92016-07-26 09:22:18 -0400273 handleToggleStateChanged(toggleState, adapter.getToggleEnabled());
Muyuan Li0e9f5382016-04-27 15:51:15 -0700274 mQsDetailHeader.setClickable(true);
275 mQsDetailHeader.setOnClickListener(new OnClickListener() {
276 @Override
277 public void onClick(View v) {
278 boolean checked = !mQsDetailHeaderSwitch.isChecked();
279 mQsDetailHeaderSwitch.setChecked(checked);
280 adapter.setToggleState(checked);
281 }
282 });
283 }
284 }
285
Fabian Kozynskidcd3b3b2020-02-18 15:05:04 -0500286 @Override
287 public WindowInsets onApplyWindowInsets(WindowInsets insets) {
288 DisplayCutout cutout = insets.getDisplayCutout();
289
290 Pair<Integer, Integer> padding = StatusBarWindowView.cornerCutoutMargins(
291 cutout, getDisplay());
292
293 if (padding == null) {
294 mQsDetailHeader.setPaddingRelative(
295 getResources().getDimensionPixelSize(R.dimen.qs_detail_header_padding),
296 getPaddingTop(),
297 getResources().getDimensionPixelSize(R.dimen.qs_detail_header_padding),
298 getPaddingBottom()
299 );
300 } else {
301 mQsDetailHeader.setPadding(
302 padding.first,
303 getPaddingTop(),
304 padding.second,
305 getPaddingBottom()
306 );
307 }
308
309 return super.onApplyWindowInsets(insets);
310 }
311
Jason Monk35a9c142016-07-25 10:18:30 -0400312 private void handleToggleStateChanged(boolean state, boolean toggleEnabled) {
Jason Monk48e093e2016-07-15 10:37:07 -0400313 mSwitchState = state;
Jason Monk35a9c142016-07-25 10:18:30 -0400314 if (mAnimatingOpen) {
Jason Monk48e093e2016-07-15 10:37:07 -0400315 return;
316 }
Fabian Kozynski8a867952019-12-02 10:32:00 -0500317 if (mQsDetailHeaderSwitch != null) mQsDetailHeaderSwitch.setChecked(state);
Jason Monkfac25382016-07-19 14:13:37 -0400318 mQsDetailHeader.setEnabled(toggleEnabled);
Fabian Kozynski8a867952019-12-02 10:32:00 -0500319 if (mQsDetailHeaderSwitch != null) mQsDetailHeaderSwitch.setEnabled(toggleEnabled);
Jason Monk377e7ad2016-02-16 14:03:21 -0500320 }
321
322 private void handleScanStateChanged(boolean state) {
323 if (mScanState == state) return;
324 mScanState = state;
325 final Animatable anim = (Animatable) mQsDetailHeaderProgress.getDrawable();
326 if (state) {
Jason Monkd9fcc642017-08-01 10:13:43 -0400327 mQsDetailHeaderProgress.animate().cancel();
328 mQsDetailHeaderProgress.animate()
329 .alpha(1)
330 .withEndAction(anim::start)
331 .start();
Jason Monk377e7ad2016-02-16 14:03:21 -0500332 } else {
Jason Monkd9fcc642017-08-01 10:13:43 -0400333 mQsDetailHeaderProgress.animate().cancel();
334 mQsDetailHeaderProgress.animate()
335 .alpha(0f)
336 .withEndAction(anim::stop)
337 .start();
Jason Monk377e7ad2016-02-16 14:03:21 -0500338 }
339 }
340
Jason Monk48e093e2016-07-15 10:37:07 -0400341 private void checkPendingAnimations() {
Jason Monk35a9c142016-07-25 10:18:30 -0400342 handleToggleStateChanged(mSwitchState,
343 mDetailAdapter != null && mDetailAdapter.getToggleEnabled());
Jason Monk48e093e2016-07-15 10:37:07 -0400344 }
345
Jason Monk46767b72016-08-18 10:58:04 -0400346 protected Callback mQsPanelCallback = new Callback() {
Jason Monk377e7ad2016-02-16 14:03:21 -0500347 @Override
348 public void onToggleStateChanged(final boolean state) {
349 post(new Runnable() {
350 @Override
351 public void run() {
Jason Monk35a9c142016-07-25 10:18:30 -0400352 handleToggleStateChanged(state,
353 mDetailAdapter != null && mDetailAdapter.getToggleEnabled());
Jason Monk377e7ad2016-02-16 14:03:21 -0500354 }
355 });
356 }
357
358 @Override
359 public void onShowingDetail(final DetailAdapter detail, final int x, final int y) {
360 post(new Runnable() {
361 @Override
362 public void run() {
Fabian Kozynski511e8b92018-10-10 12:31:35 -0400363 if (isAttachedToWindow()) {
364 handleShowingDetail(detail, x, y, false /* toggleQs */);
365 }
Jason Monk377e7ad2016-02-16 14:03:21 -0500366 }
367 });
368 }
369
370 @Override
371 public void onScanStateChanged(final boolean state) {
372 post(new Runnable() {
373 @Override
374 public void run() {
375 handleScanStateChanged(state);
376 }
377 });
378 }
379 };
380
381 private final AnimatorListenerAdapter mHideGridContentWhenDone = new AnimatorListenerAdapter() {
382 public void onAnimationCancel(Animator animation) {
383 // If we have been cancelled, remove the listener so that onAnimationEnd doesn't get
384 // called, this will avoid accidentally turning off the grid when we don't want to.
385 animation.removeListener(this);
Jason Monk35a9c142016-07-25 10:18:30 -0400386 mAnimatingOpen = false;
Jason Monk48e093e2016-07-15 10:37:07 -0400387 checkPendingAnimations();
Jason Monk377e7ad2016-02-16 14:03:21 -0500388 };
389
390 @Override
391 public void onAnimationEnd(Animator animation) {
392 // Only hide content if still in detail state.
393 if (mDetailAdapter != null) {
394 mQsPanel.setGridContentVisibility(false);
Jason Monk589bb702016-04-12 15:44:43 -0400395 mHeader.setVisibility(View.INVISIBLE);
Jason Monkb4ec0b92017-06-13 13:47:54 -0400396 mFooter.setVisibility(View.INVISIBLE);
Jason Monk377e7ad2016-02-16 14:03:21 -0500397 }
Jason Monk35a9c142016-07-25 10:18:30 -0400398 mAnimatingOpen = false;
Jason Monk48e093e2016-07-15 10:37:07 -0400399 checkPendingAnimations();
Jason Monk377e7ad2016-02-16 14:03:21 -0500400 }
401 };
402
403 private final AnimatorListenerAdapter mTeardownDetailWhenDone = new AnimatorListenerAdapter() {
404 public void onAnimationEnd(Animator animation) {
405 mDetailContent.removeAllViews();
406 setVisibility(View.INVISIBLE);
407 mClosingDetail = false;
408 };
409 };
410}