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.graphics.drawable.Animatable; 27 import android.util.AttributeSet; 28 import android.util.Pair; 29 import android.util.SparseArray; 30 import android.view.DisplayCutout; 31 import android.view.View; 32 import android.view.ViewGroup; 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.logging.MetricsLogger; 41 import com.android.systemui.Dependency; 42 import com.android.systemui.FontSizeUtils; 43 import com.android.systemui.R; 44 import com.android.systemui.SysUiServiceProvider; 45 import com.android.systemui.plugins.ActivityStarter; 46 import com.android.systemui.plugins.qs.DetailAdapter; 47 import com.android.systemui.statusbar.CommandQueue; 48 import com.android.systemui.statusbar.phone.PhoneStatusBarView; 49 50 public class QSDetail extends LinearLayout { 51 52 private static final String TAG = "QSDetail"; 53 private static final long FADE_DURATION = 300; 54 55 private final SparseArray<View> mDetailViews = new SparseArray<>(); 56 57 private ViewGroup mDetailContent; 58 protected TextView mDetailSettingsButton; 59 protected TextView mDetailDoneButton; 60 private QSDetailClipper mClipper; 61 private DetailAdapter mDetailAdapter; 62 private QSPanel mQsPanel; 63 64 protected View mQsDetailHeader; 65 protected TextView mQsDetailHeaderTitle; 66 protected Switch mQsDetailHeaderSwitch; 67 protected ImageView mQsDetailHeaderProgress; 68 69 protected QSTileHost mHost; 70 71 private boolean mScanState; 72 private boolean mClosingDetail; 73 private boolean mFullyExpanded; 74 private QuickStatusBarHeader mHeader; 75 private boolean mTriggeredExpand; 76 private int mOpenX; 77 private int mOpenY; 78 private boolean mAnimatingOpen; 79 private boolean mSwitchState; 80 private View mFooter; 81 QSDetail(Context context, @Nullable AttributeSet attrs)82 public QSDetail(Context context, @Nullable AttributeSet attrs) { 83 super(context, attrs); 84 } 85 86 @Override onConfigurationChanged(Configuration newConfig)87 protected void onConfigurationChanged(Configuration newConfig) { 88 super.onConfigurationChanged(newConfig); 89 FontSizeUtils.updateFontSize(mDetailDoneButton, R.dimen.qs_detail_button_text_size); 90 FontSizeUtils.updateFontSize(mDetailSettingsButton, R.dimen.qs_detail_button_text_size); 91 92 for (int i = 0; i < mDetailViews.size(); i++) { 93 mDetailViews.valueAt(i).dispatchConfigurationChanged(newConfig); 94 } 95 } 96 97 @Override onFinishInflate()98 protected void onFinishInflate() { 99 super.onFinishInflate(); 100 mDetailContent = findViewById(android.R.id.content); 101 mDetailSettingsButton = findViewById(android.R.id.button2); 102 mDetailDoneButton = findViewById(android.R.id.button1); 103 104 mQsDetailHeader = findViewById(R.id.qs_detail_header); 105 mQsDetailHeaderTitle = (TextView) mQsDetailHeader.findViewById(android.R.id.title); 106 mQsDetailHeaderSwitch = (Switch) mQsDetailHeader.findViewById(android.R.id.toggle); 107 mQsDetailHeaderProgress = findViewById(R.id.qs_detail_header_progress); 108 109 updateDetailText(); 110 111 mClipper = new QSDetailClipper(this); 112 113 final OnClickListener doneListener = new OnClickListener() { 114 @Override 115 public void onClick(View v) { 116 announceForAccessibility( 117 mContext.getString(R.string.accessibility_desc_quick_settings)); 118 mQsPanel.closeDetail(); 119 } 120 }; 121 mDetailDoneButton.setOnClickListener(doneListener); 122 } 123 setQsPanel(QSPanel panel, QuickStatusBarHeader header, View footer)124 public void setQsPanel(QSPanel panel, QuickStatusBarHeader header, View footer) { 125 mQsPanel = panel; 126 mHeader = header; 127 mFooter = footer; 128 mHeader.setCallback(mQsPanelCallback); 129 mQsPanel.setCallback(mQsPanelCallback); 130 } 131 setHost(QSTileHost host)132 public void setHost(QSTileHost host) { 133 mHost = host; 134 } isShowingDetail()135 public boolean isShowingDetail() { 136 return mDetailAdapter != null; 137 } 138 setFullyExpanded(boolean fullyExpanded)139 public void setFullyExpanded(boolean fullyExpanded) { 140 mFullyExpanded = fullyExpanded; 141 } 142 setExpanded(boolean qsExpanded)143 public void setExpanded(boolean qsExpanded) { 144 if (!qsExpanded) { 145 mTriggeredExpand = false; 146 } 147 } 148 149 @Override onApplyWindowInsets(WindowInsets insets)150 public WindowInsets onApplyWindowInsets(WindowInsets insets) { 151 DisplayCutout cutout = insets.getDisplayCutout(); 152 Pair<Integer, Integer> padding = PhoneStatusBarView.cornerCutoutMargins( 153 cutout, getDisplay()); 154 if (padding == null) { 155 mQsDetailHeader.setPaddingRelative( 156 getResources().getDimensionPixelSize(R.dimen.qs_detail_header_padding), 157 getPaddingTop(), 158 getResources().getDimensionPixelSize(R.dimen.qs_detail_header_padding), 159 getPaddingBottom() 160 ); 161 } else { 162 mQsDetailHeader.setPadding(padding.first, getPaddingTop(), 163 padding.second, getPaddingBottom()); 164 } 165 return super.onApplyWindowInsets(insets); 166 } 167 updateDetailText()168 private void updateDetailText() { 169 mDetailDoneButton.setText(R.string.quick_settings_done); 170 mDetailSettingsButton.setText(R.string.quick_settings_more_settings); 171 } 172 updateResources()173 public void updateResources() { 174 updateDetailText(); 175 } 176 isClosingDetail()177 public boolean isClosingDetail() { 178 return mClosingDetail; 179 } 180 181 public interface Callback { onShowingDetail(DetailAdapter detail, int x, int y)182 void onShowingDetail(DetailAdapter detail, int x, int y); onToggleStateChanged(boolean state)183 void onToggleStateChanged(boolean state); onScanStateChanged(boolean state)184 void onScanStateChanged(boolean state); 185 } 186 handleShowingDetail(final DetailAdapter adapter, int x, int y, boolean toggleQs)187 public void handleShowingDetail(final DetailAdapter adapter, int x, int y, 188 boolean toggleQs) { 189 final boolean showingDetail = adapter != null; 190 setClickable(showingDetail); 191 if (showingDetail) { 192 setupDetailHeader(adapter); 193 if (toggleQs && !mFullyExpanded) { 194 mTriggeredExpand = true; 195 SysUiServiceProvider.getComponent(mContext, CommandQueue.class) 196 .animateExpandSettingsPanel(null); 197 } else { 198 mTriggeredExpand = false; 199 } 200 mOpenX = x; 201 mOpenY = y; 202 } else { 203 // Ensure we collapse into the same point we opened from. 204 x = mOpenX; 205 y = mOpenY; 206 if (toggleQs && mTriggeredExpand) { 207 SysUiServiceProvider.getComponent(mContext, CommandQueue.class) 208 .animateCollapsePanels(); 209 mTriggeredExpand = false; 210 } 211 } 212 213 boolean visibleDiff = (mDetailAdapter != null) != (adapter != null); 214 if (!visibleDiff && mDetailAdapter == adapter) return; // already in right state 215 AnimatorListener listener = null; 216 if (adapter != null) { 217 int viewCacheIndex = adapter.getMetricsCategory(); 218 View detailView = adapter.createDetailView(mContext, mDetailViews.get(viewCacheIndex), 219 mDetailContent); 220 if (detailView == null) throw new IllegalStateException("Must return detail view"); 221 222 setupDetailFooter(adapter); 223 224 mDetailContent.removeAllViews(); 225 mDetailContent.addView(detailView); 226 mDetailViews.put(viewCacheIndex, detailView); 227 Dependency.get(MetricsLogger.class).visible(adapter.getMetricsCategory()); 228 announceForAccessibility(mContext.getString( 229 R.string.accessibility_quick_settings_detail, 230 adapter.getTitle())); 231 mDetailAdapter = adapter; 232 listener = mHideGridContentWhenDone; 233 setVisibility(View.VISIBLE); 234 } else { 235 if (mDetailAdapter != null) { 236 Dependency.get(MetricsLogger.class).hidden(mDetailAdapter.getMetricsCategory()); 237 } 238 mClosingDetail = true; 239 mDetailAdapter = null; 240 listener = mTeardownDetailWhenDone; 241 mHeader.setVisibility(View.VISIBLE); 242 mFooter.setVisibility(View.VISIBLE); 243 mQsPanel.setGridContentVisibility(true); 244 mQsPanelCallback.onScanStateChanged(false); 245 } 246 sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); 247 248 animateDetailVisibleDiff(x, y, visibleDiff, listener); 249 } 250 animateDetailVisibleDiff(int x, int y, boolean visibleDiff, AnimatorListener listener)251 protected void animateDetailVisibleDiff(int x, int y, boolean visibleDiff, AnimatorListener listener) { 252 if (visibleDiff) { 253 mAnimatingOpen = mDetailAdapter != null; 254 if (mFullyExpanded || mDetailAdapter != null) { 255 setAlpha(1); 256 mClipper.animateCircularClip(x, y, mDetailAdapter != null, listener); 257 } else { 258 animate().alpha(0) 259 .setDuration(FADE_DURATION) 260 .setListener(listener) 261 .start(); 262 } 263 } 264 } 265 setupDetailFooter(DetailAdapter adapter)266 protected void setupDetailFooter(DetailAdapter adapter) { 267 final Intent settingsIntent = adapter.getSettingsIntent(); 268 mDetailSettingsButton.setVisibility(settingsIntent != null ? VISIBLE : GONE); 269 mDetailSettingsButton.setOnClickListener(v -> { 270 Dependency.get(MetricsLogger.class).action(ACTION_QS_MORE_SETTINGS, 271 adapter.getMetricsCategory()); 272 Dependency.get(ActivityStarter.class) 273 .postStartActivityDismissingKeyguard(settingsIntent, 0); 274 }); 275 } 276 setupDetailHeader(final DetailAdapter adapter)277 protected void setupDetailHeader(final DetailAdapter adapter) { 278 mQsDetailHeaderTitle.setText(adapter.getTitle()); 279 final Boolean toggleState = adapter.getToggleState(); 280 if (toggleState == null) { 281 mQsDetailHeaderSwitch.setVisibility(INVISIBLE); 282 mQsDetailHeader.setClickable(false); 283 } else { 284 mQsDetailHeaderSwitch.setVisibility(VISIBLE); 285 handleToggleStateChanged(toggleState, adapter.getToggleEnabled()); 286 mQsDetailHeader.setClickable(true); 287 mQsDetailHeader.setOnClickListener(new OnClickListener() { 288 @Override 289 public void onClick(View v) { 290 boolean checked = !mQsDetailHeaderSwitch.isChecked(); 291 mQsDetailHeaderSwitch.setChecked(checked); 292 adapter.setToggleState(checked); 293 } 294 }); 295 } 296 } 297 handleToggleStateChanged(boolean state, boolean toggleEnabled)298 private void handleToggleStateChanged(boolean state, boolean toggleEnabled) { 299 mSwitchState = state; 300 if (mAnimatingOpen) { 301 return; 302 } 303 mQsDetailHeaderSwitch.setChecked(state); 304 mQsDetailHeader.setEnabled(toggleEnabled); 305 mQsDetailHeaderSwitch.setEnabled(toggleEnabled); 306 } 307 handleScanStateChanged(boolean state)308 private void handleScanStateChanged(boolean state) { 309 if (mScanState == state) return; 310 mScanState = state; 311 final Animatable anim = (Animatable) mQsDetailHeaderProgress.getDrawable(); 312 if (state) { 313 mQsDetailHeaderProgress.animate().cancel(); 314 mQsDetailHeaderProgress.animate() 315 .alpha(1) 316 .withEndAction(anim::start) 317 .start(); 318 } else { 319 mQsDetailHeaderProgress.animate().cancel(); 320 mQsDetailHeaderProgress.animate() 321 .alpha(0f) 322 .withEndAction(anim::stop) 323 .start(); 324 } 325 } 326 checkPendingAnimations()327 private void checkPendingAnimations() { 328 handleToggleStateChanged(mSwitchState, 329 mDetailAdapter != null && mDetailAdapter.getToggleEnabled()); 330 } 331 332 protected Callback mQsPanelCallback = new Callback() { 333 @Override 334 public void onToggleStateChanged(final boolean state) { 335 post(new Runnable() { 336 @Override 337 public void run() { 338 handleToggleStateChanged(state, 339 mDetailAdapter != null && mDetailAdapter.getToggleEnabled()); 340 } 341 }); 342 } 343 344 @Override 345 public void onShowingDetail(final DetailAdapter detail, final int x, final int y) { 346 post(new Runnable() { 347 @Override 348 public void run() { 349 if (isAttachedToWindow()) { 350 handleShowingDetail(detail, x, y, false /* toggleQs */); 351 } 352 } 353 }); 354 } 355 356 @Override 357 public void onScanStateChanged(final boolean state) { 358 post(new Runnable() { 359 @Override 360 public void run() { 361 handleScanStateChanged(state); 362 } 363 }); 364 } 365 }; 366 367 private final AnimatorListenerAdapter mHideGridContentWhenDone = new AnimatorListenerAdapter() { 368 public void onAnimationCancel(Animator animation) { 369 // If we have been cancelled, remove the listener so that onAnimationEnd doesn't get 370 // called, this will avoid accidentally turning off the grid when we don't want to. 371 animation.removeListener(this); 372 mAnimatingOpen = false; 373 checkPendingAnimations(); 374 }; 375 376 @Override 377 public void onAnimationEnd(Animator animation) { 378 // Only hide content if still in detail state. 379 if (mDetailAdapter != null) { 380 mQsPanel.setGridContentVisibility(false); 381 mHeader.setVisibility(View.INVISIBLE); 382 mFooter.setVisibility(View.INVISIBLE); 383 } 384 mAnimatingOpen = false; 385 checkPendingAnimations(); 386 } 387 }; 388 389 private final AnimatorListenerAdapter mTeardownDetailWhenDone = new AnimatorListenerAdapter() { 390 public void onAnimationEnd(Animator animation) { 391 mDetailContent.removeAllViews(); 392 setVisibility(View.INVISIBLE); 393 mClosingDetail = false; 394 }; 395 }; 396 } 397