1 /* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.systemui.qs; 18 19 import android.animation.Animator; 20 import android.animation.Animator.AnimatorListener; 21 import android.animation.AnimatorListenerAdapter; 22 import android.content.Context; 23 import android.content.Intent; 24 import android.content.res.Configuration; 25 import android.content.res.Resources; 26 import android.os.Handler; 27 import android.os.Message; 28 import android.util.AttributeSet; 29 import android.view.LayoutInflater; 30 import android.view.View; 31 import android.view.ViewGroup; 32 import android.view.accessibility.AccessibilityEvent; 33 import android.widget.ImageView; 34 import android.widget.TextView; 35 36 import com.android.internal.logging.MetricsLogger; 37 import com.android.systemui.FontSizeUtils; 38 import com.android.systemui.R; 39 import com.android.systemui.qs.QSTile.DetailAdapter; 40 import com.android.systemui.settings.BrightnessController; 41 import com.android.systemui.settings.ToggleSlider; 42 import com.android.systemui.statusbar.phone.QSTileHost; 43 import com.android.systemui.statusbar.policy.BrightnessMirrorController; 44 45 import java.util.ArrayList; 46 import java.util.Collection; 47 48 /** View that represents the quick settings tile panel. **/ 49 public class QSPanel extends ViewGroup { 50 private static final float TILE_ASPECT = 1.2f; 51 52 private final Context mContext; 53 protected final ArrayList<TileRecord> mRecords = new ArrayList<TileRecord>(); 54 private final View mDetail; 55 private final ViewGroup mDetailContent; 56 private final TextView mDetailSettingsButton; 57 private final TextView mDetailDoneButton; 58 protected final View mBrightnessView; 59 private final QSDetailClipper mClipper; 60 private final H mHandler = new H(); 61 62 private int mColumns; 63 private int mCellWidth; 64 private int mCellHeight; 65 private int mLargeCellWidth; 66 private int mLargeCellHeight; 67 private int mPanelPaddingBottom; 68 private int mDualTileUnderlap; 69 private int mBrightnessPaddingTop; 70 private int mGridHeight; 71 private boolean mExpanded; 72 private boolean mListening; 73 private boolean mClosingDetail; 74 75 private Record mDetailRecord; 76 private Callback mCallback; 77 private BrightnessController mBrightnessController; 78 private QSTileHost mHost; 79 80 private QSFooter mFooter; 81 private boolean mGridContentVisible = true; 82 83 public QSPanel(Context context) { 84 this(context, null); 85 } 86 87 public QSPanel(Context context, AttributeSet attrs) { 88 super(context, attrs); 89 mContext = context; 90 91 mDetail = LayoutInflater.from(context).inflate(R.layout.qs_detail, this, false); 92 mDetailContent = (ViewGroup) mDetail.findViewById(android.R.id.content); 93 mDetailSettingsButton = (TextView) mDetail.findViewById(android.R.id.button2); 94 mDetailDoneButton = (TextView) mDetail.findViewById(android.R.id.button1); 95 updateDetailText(); 96 mDetail.setVisibility(GONE); 97 mDetail.setClickable(true); 98 mBrightnessView = LayoutInflater.from(context).inflate( 99 R.layout.quick_settings_brightness_dialog, this, false); 100 mFooter = new QSFooter(this, context); 101 addView(mDetail); 102 addView(mBrightnessView); 103 addView(mFooter.getView()); 104 mClipper = new QSDetailClipper(mDetail); 105 updateResources(); 106 107 mBrightnessController = new BrightnessController(getContext(), 108 (ImageView) findViewById(R.id.brightness_icon), 109 (ToggleSlider) findViewById(R.id.brightness_slider)); 110 111 mDetailDoneButton.setOnClickListener(new OnClickListener() { 112 @Override 113 public void onClick(View v) { 114 announceForAccessibility( 115 mContext.getString(R.string.accessibility_desc_quick_settings)); 116 closeDetail(); 117 } 118 }); 119 } 120 121 private void updateDetailText() { 122 mDetailDoneButton.setText(R.string.quick_settings_done); 123 mDetailSettingsButton.setText(R.string.quick_settings_more_settings); 124 } 125 126 public void setBrightnessMirror(BrightnessMirrorController c) { 127 super.onFinishInflate(); 128 ToggleSlider brightnessSlider = (ToggleSlider) findViewById(R.id.brightness_slider); 129 ToggleSlider mirror = (ToggleSlider) c.getMirror().findViewById(R.id.brightness_slider); 130 brightnessSlider.setMirror(mirror); 131 brightnessSlider.setMirrorController(c); 132 } 133 134 public void setCallback(Callback callback) { 135 mCallback = callback; 136 } 137 138 public void setHost(QSTileHost host) { 139 mHost = host; 140 mFooter.setHost(host); 141 } 142 143 public QSTileHost getHost() { 144 return mHost; 145 } 146 147 public void updateResources() { 148 final Resources res = mContext.getResources(); 149 final int columns = Math.max(1, res.getInteger(R.integer.quick_settings_num_columns)); 150 mCellHeight = res.getDimensionPixelSize(R.dimen.qs_tile_height); 151 mCellWidth = (int)(mCellHeight * TILE_ASPECT); 152 mLargeCellHeight = res.getDimensionPixelSize(R.dimen.qs_dual_tile_height); 153 mLargeCellWidth = (int)(mLargeCellHeight * TILE_ASPECT); 154 mPanelPaddingBottom = res.getDimensionPixelSize(R.dimen.qs_panel_padding_bottom); 155 mDualTileUnderlap = res.getDimensionPixelSize(R.dimen.qs_dual_tile_padding_vertical); 156 mBrightnessPaddingTop = res.getDimensionPixelSize(R.dimen.qs_brightness_padding_top); 157 if (mColumns != columns) { 158 mColumns = columns; 159 postInvalidate(); 160 } 161 for (TileRecord r : mRecords) { 162 r.tile.clearState(); 163 } 164 if (mListening) { 165 refreshAllTiles(); 166 } 167 updateDetailText(); 168 } 169 170 @Override 171 protected void onConfigurationChanged(Configuration newConfig) { 172 super.onConfigurationChanged(newConfig); 173 FontSizeUtils.updateFontSize(mDetailDoneButton, R.dimen.qs_detail_button_text_size); 174 FontSizeUtils.updateFontSize(mDetailSettingsButton, R.dimen.qs_detail_button_text_size); 175 176 // We need to poke the detail views as well as they might not be attached to the view 177 // hierarchy but reused at a later point. 178 int count = mRecords.size(); 179 for (int i = 0; i < count; i++) { 180 View detailView = mRecords.get(i).detailView; 181 if (detailView != null) { 182 detailView.dispatchConfigurationChanged(newConfig); 183 } 184 } 185 mFooter.onConfigurationChanged(); 186 } 187 188 public void setExpanded(boolean expanded) { 189 if (mExpanded == expanded) return; 190 mExpanded = expanded; 191 MetricsLogger.visibility(mContext, MetricsLogger.QS_PANEL, mExpanded); 192 if (!mExpanded) { 193 closeDetail(); 194 } else { 195 logTiles(); 196 } 197 } 198 199 public void setListening(boolean listening) { 200 if (mListening == listening) return; 201 mListening = listening; 202 for (TileRecord r : mRecords) { 203 r.tile.setListening(mListening); 204 } 205 mFooter.setListening(mListening); 206 if (mListening) { 207 refreshAllTiles(); 208 } 209 if (listening) { 210 mBrightnessController.registerCallbacks(); 211 } else { 212 mBrightnessController.unregisterCallbacks(); 213 } 214 } 215 216 public void refreshAllTiles() { 217 for (TileRecord r : mRecords) { 218 r.tile.refreshState(); 219 } 220 mFooter.refreshState(); 221 } 222 223 public void showDetailAdapter(boolean show, DetailAdapter adapter, int[] locationInWindow) { 224 int xInWindow = locationInWindow[0]; 225 int yInWindow = locationInWindow[1]; 226 mDetail.getLocationInWindow(locationInWindow); 227 228 Record r = new Record(); 229 r.detailAdapter = adapter; 230 r.x = xInWindow - locationInWindow[0]; 231 r.y = yInWindow - locationInWindow[1]; 232 233 locationInWindow[0] = xInWindow; 234 locationInWindow[1] = yInWindow; 235 236 showDetail(show, r); 237 } 238 239 private void showDetail(boolean show, Record r) { 240 mHandler.obtainMessage(H.SHOW_DETAIL, show ? 1 : 0, 0, r).sendToTarget(); 241 } 242 243 private void setTileVisibility(View v, int visibility) { 244 mHandler.obtainMessage(H.SET_TILE_VISIBILITY, visibility, 0, v).sendToTarget(); 245 } 246 247 private void handleSetTileVisibility(View v, int visibility) { 248 if (visibility == VISIBLE && !mGridContentVisible) { 249 visibility = INVISIBLE; 250 } 251 if (visibility == v.getVisibility()) return; 252 v.setVisibility(visibility); 253 } 254 255 public void setTiles(Collection<QSTile<?>> tiles) { 256 for (TileRecord record : mRecords) { 257 removeView(record.tileView); 258 } 259 mRecords.clear(); 260 for (QSTile<?> tile : tiles) { 261 addTile(tile); 262 } 263 if (isShowingDetail()) { 264 mDetail.bringToFront(); 265 } 266 } 267 268 private void drawTile(TileRecord r, QSTile.State state) { 269 final int visibility = state.visible ? VISIBLE : GONE; 270 setTileVisibility(r.tileView, visibility); 271 r.tileView.onStateChanged(state); 272 } 273 274 private void addTile(final QSTile<?> tile) { 275 final TileRecord r = new TileRecord(); 276 r.tile = tile; 277 r.tileView = tile.createTileView(mContext); 278 r.tileView.setVisibility(View.GONE); 279 final QSTile.Callback callback = new QSTile.Callback() { 280 @Override 281 public void onStateChanged(QSTile.State state) { 282 if (!r.openingDetail) { 283 drawTile(r, state); 284 } 285 } 286 @Override 287 public void onShowDetail(boolean show) { 288 QSPanel.this.showDetail(show, r); 289 } 290 @Override 291 public void onToggleStateChanged(boolean state) { 292 if (mDetailRecord == r) { 293 fireToggleStateChanged(state); 294 } 295 } 296 @Override 297 public void onScanStateChanged(boolean state) { 298 r.scanState = state; 299 if (mDetailRecord == r) { 300 fireScanStateChanged(r.scanState); 301 } 302 } 303 304 @Override 305 public void onAnnouncementRequested(CharSequence announcement) { 306 announceForAccessibility(announcement); 307 } 308 }; 309 r.tile.setCallback(callback); 310 final View.OnClickListener click = new View.OnClickListener() { 311 @Override 312 public void onClick(View v) { 313 r.tile.click(); 314 } 315 }; 316 final View.OnClickListener clickSecondary = new View.OnClickListener() { 317 @Override 318 public void onClick(View v) { 319 r.tile.secondaryClick(); 320 } 321 }; 322 final View.OnLongClickListener longClick = new View.OnLongClickListener() { 323 @Override 324 public boolean onLongClick(View v) { 325 r.tile.longClick(); 326 return true; 327 } 328 }; 329 r.tileView.init(click, clickSecondary, longClick); 330 r.tile.setListening(mListening); 331 callback.onStateChanged(r.tile.getState()); 332 r.tile.refreshState(); 333 mRecords.add(r); 334 335 addView(r.tileView); 336 } 337 338 public boolean isShowingDetail() { 339 return mDetailRecord != null; 340 } 341 342 public void closeDetail() { 343 showDetail(false, mDetailRecord); 344 } 345 346 public boolean isClosingDetail() { 347 return mClosingDetail; 348 } 349 350 public int getGridHeight() { 351 return mGridHeight; 352 } 353 354 private void handleShowDetail(Record r, boolean show) { 355 if (r instanceof TileRecord) { 356 handleShowDetailTile((TileRecord) r, show); 357 } else { 358 int x = 0; 359 int y = 0; 360 if (r != null) { 361 x = r.x; 362 y = r.y; 363 } 364 handleShowDetailImpl(r, show, x, y); 365 } 366 } 367 368 private void handleShowDetailTile(TileRecord r, boolean show) { 369 if ((mDetailRecord != null) == show && mDetailRecord == r) return; 370 371 if (show) { 372 r.detailAdapter = r.tile.getDetailAdapter(); 373 if (r.detailAdapter == null) return; 374 } 375 r.tile.setDetailListening(show); 376 int x = r.tileView.getLeft() + r.tileView.getWidth() / 2; 377 int y = r.tileView.getTop() + r.tileView.getHeight() / 2; 378 handleShowDetailImpl(r, show, x, y); 379 } 380 381 private void handleShowDetailImpl(Record r, boolean show, int x, int y) { 382 boolean visibleDiff = (mDetailRecord != null) != show; 383 if (!visibleDiff && mDetailRecord == r) return; // already in right state 384 DetailAdapter detailAdapter = null; 385 AnimatorListener listener = null; 386 if (show) { 387 detailAdapter = r.detailAdapter; 388 r.detailView = detailAdapter.createDetailView(mContext, r.detailView, mDetailContent); 389 if (r.detailView == null) throw new IllegalStateException("Must return detail view"); 390 391 final Intent settingsIntent = detailAdapter.getSettingsIntent(); 392 mDetailSettingsButton.setVisibility(settingsIntent != null ? VISIBLE : GONE); 393 mDetailSettingsButton.setOnClickListener(new OnClickListener() { 394 @Override 395 public void onClick(View v) { 396 mHost.startActivityDismissingKeyguard(settingsIntent); 397 } 398 }); 399 400 mDetailContent.removeAllViews(); 401 mDetail.bringToFront(); 402 mDetailContent.addView(r.detailView); 403 MetricsLogger.visible(mContext, detailAdapter.getMetricsCategory()); 404 announceForAccessibility(mContext.getString( 405 R.string.accessibility_quick_settings_detail, 406 mContext.getString(detailAdapter.getTitle()))); 407 setDetailRecord(r); 408 listener = mHideGridContentWhenDone; 409 if (r instanceof TileRecord && visibleDiff) { 410 ((TileRecord) r).openingDetail = true; 411 } 412 } else { 413 if (mDetailRecord != null) { 414 MetricsLogger.hidden(mContext, mDetailRecord.detailAdapter.getMetricsCategory()); 415 } 416 mClosingDetail = true; 417 setGridContentVisibility(true); 418 listener = mTeardownDetailWhenDone; 419 fireScanStateChanged(false); 420 } 421 sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); 422 fireShowingDetail(show ? detailAdapter : null); 423 if (visibleDiff) { 424 mClipper.animateCircularClip(x, y, show, listener); 425 } 426 } 427 428 private void setGridContentVisibility(boolean visible) { 429 int newVis = visible ? VISIBLE : INVISIBLE; 430 for (int i = 0; i < mRecords.size(); i++) { 431 TileRecord tileRecord = mRecords.get(i); 432 if (tileRecord.tileView.getVisibility() != GONE) { 433 tileRecord.tileView.setVisibility(newVis); 434 } 435 } 436 mBrightnessView.setVisibility(newVis); 437 if (mGridContentVisible != visible) { 438 MetricsLogger.visibility(mContext, MetricsLogger.QS_PANEL, newVis); 439 } 440 mGridContentVisible = visible; 441 } 442 443 private void logTiles() { 444 for (int i = 0; i < mRecords.size(); i++) { 445 TileRecord tileRecord = mRecords.get(i); 446 if (tileRecord.tile.getState().visible) { 447 MetricsLogger.visible(mContext, tileRecord.tile.getMetricsCategory()); 448 } 449 } 450 } 451 452 @Override 453 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 454 final int width = MeasureSpec.getSize(widthMeasureSpec); 455 mBrightnessView.measure(exactly(width), MeasureSpec.UNSPECIFIED); 456 final int brightnessHeight = mBrightnessView.getMeasuredHeight() + mBrightnessPaddingTop; 457 mFooter.getView().measure(exactly(width), MeasureSpec.UNSPECIFIED); 458 int r = -1; 459 int c = -1; 460 int rows = 0; 461 boolean rowIsDual = false; 462 for (TileRecord record : mRecords) { 463 if (record.tileView.getVisibility() == GONE) continue; 464 // wrap to next column if we've reached the max # of columns 465 // also don't allow dual + single tiles on the same row 466 if (r == -1 || c == (mColumns - 1) || rowIsDual != record.tile.supportsDualTargets()) { 467 r++; 468 c = 0; 469 rowIsDual = record.tile.supportsDualTargets(); 470 } else { 471 c++; 472 } 473 record.row = r; 474 record.col = c; 475 rows = r + 1; 476 } 477 478 View previousView = mBrightnessView; 479 for (TileRecord record : mRecords) { 480 if (record.tileView.setDual(record.tile.supportsDualTargets())) { 481 record.tileView.handleStateChanged(record.tile.getState()); 482 } 483 if (record.tileView.getVisibility() == GONE) continue; 484 final int cw = record.row == 0 ? mLargeCellWidth : mCellWidth; 485 final int ch = record.row == 0 ? mLargeCellHeight : mCellHeight; 486 record.tileView.measure(exactly(cw), exactly(ch)); 487 previousView = record.tileView.updateAccessibilityOrder(previousView); 488 } 489 int h = rows == 0 ? brightnessHeight : (getRowTop(rows) + mPanelPaddingBottom); 490 if (mFooter.hasFooter()) { 491 h += mFooter.getView().getMeasuredHeight(); 492 } 493 mDetail.measure(exactly(width), MeasureSpec.UNSPECIFIED); 494 if (mDetail.getMeasuredHeight() < h) { 495 mDetail.measure(exactly(width), exactly(h)); 496 } 497 mGridHeight = h; 498 setMeasuredDimension(width, Math.max(h, mDetail.getMeasuredHeight())); 499 } 500 501 private static int exactly(int size) { 502 return MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY); 503 } 504 505 @Override 506 protected void onLayout(boolean changed, int l, int t, int r, int b) { 507 final int w = getWidth(); 508 mBrightnessView.layout(0, mBrightnessPaddingTop, 509 mBrightnessView.getMeasuredWidth(), 510 mBrightnessPaddingTop + mBrightnessView.getMeasuredHeight()); 511 boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL; 512 for (TileRecord record : mRecords) { 513 if (record.tileView.getVisibility() == GONE) continue; 514 final int cols = getColumnCount(record.row); 515 final int cw = record.row == 0 ? mLargeCellWidth : mCellWidth; 516 final int extra = (w - cw * cols) / (cols + 1); 517 int left = record.col * cw + (record.col + 1) * extra; 518 final int top = getRowTop(record.row); 519 int right; 520 int tileWith = record.tileView.getMeasuredWidth(); 521 if (isRtl) { 522 right = w - left; 523 left = right - tileWith; 524 } else { 525 right = left + tileWith; 526 } 527 record.tileView.layout(left, top, right, top + record.tileView.getMeasuredHeight()); 528 } 529 final int dh = Math.max(mDetail.getMeasuredHeight(), getMeasuredHeight()); 530 mDetail.layout(0, 0, mDetail.getMeasuredWidth(), dh); 531 if (mFooter.hasFooter()) { 532 View footer = mFooter.getView(); 533 footer.layout(0, getMeasuredHeight() - footer.getMeasuredHeight(), 534 footer.getMeasuredWidth(), getMeasuredHeight()); 535 } 536 } 537 538 private int getRowTop(int row) { 539 if (row <= 0) return mBrightnessView.getMeasuredHeight() + mBrightnessPaddingTop; 540 return mBrightnessView.getMeasuredHeight() + mBrightnessPaddingTop 541 + mLargeCellHeight - mDualTileUnderlap + (row - 1) * mCellHeight; 542 } 543 544 private int getColumnCount(int row) { 545 int cols = 0; 546 for (TileRecord record : mRecords) { 547 if (record.tileView.getVisibility() == GONE) continue; 548 if (record.row == row) cols++; 549 } 550 return cols; 551 } 552 553 private void fireShowingDetail(QSTile.DetailAdapter detail) { 554 if (mCallback != null) { 555 mCallback.onShowingDetail(detail); 556 } 557 } 558 559 private void fireToggleStateChanged(boolean state) { 560 if (mCallback != null) { 561 mCallback.onToggleStateChanged(state); 562 } 563 } 564 565 private void fireScanStateChanged(boolean state) { 566 if (mCallback != null) { 567 mCallback.onScanStateChanged(state); 568 } 569 } 570 571 private void setDetailRecord(Record r) { 572 if (r == mDetailRecord) return; 573 mDetailRecord = r; 574 final boolean scanState = mDetailRecord instanceof TileRecord 575 && ((TileRecord) mDetailRecord).scanState; 576 fireScanStateChanged(scanState); 577 } 578 579 private class H extends Handler { 580 private static final int SHOW_DETAIL = 1; 581 private static final int SET_TILE_VISIBILITY = 2; 582 @Override 583 public void handleMessage(Message msg) { 584 if (msg.what == SHOW_DETAIL) { 585 handleShowDetail((Record)msg.obj, msg.arg1 != 0); 586 } else if (msg.what == SET_TILE_VISIBILITY) { 587 handleSetTileVisibility((View)msg.obj, msg.arg1); 588 } 589 } 590 } 591 592 private static class Record { 593 View detailView; 594 DetailAdapter detailAdapter; 595 int x; 596 int y; 597 } 598 599 protected static final class TileRecord extends Record { 600 public QSTile<?> tile; 601 public QSTileView tileView; 602 public int row; 603 public int col; 604 public boolean scanState; 605 public boolean openingDetail; 606 } 607 608 private final AnimatorListenerAdapter mTeardownDetailWhenDone = new AnimatorListenerAdapter() { 609 public void onAnimationEnd(Animator animation) { 610 mDetailContent.removeAllViews(); 611 setDetailRecord(null); 612 mClosingDetail = false; 613 }; 614 }; 615 616 private final AnimatorListenerAdapter mHideGridContentWhenDone = new AnimatorListenerAdapter() { 617 public void onAnimationCancel(Animator animation) { 618 // If we have been cancelled, remove the listener so that onAnimationEnd doesn't get 619 // called, this will avoid accidentally turning off the grid when we don't want to. 620 animation.removeListener(this); 621 redrawTile(); 622 }; 623 624 @Override 625 public void onAnimationEnd(Animator animation) { 626 // Only hide content if still in detail state. 627 if (mDetailRecord != null) { 628 setGridContentVisibility(false); 629 redrawTile(); 630 } 631 } 632 633 private void redrawTile() { 634 if (mDetailRecord instanceof TileRecord) { 635 final TileRecord tileRecord = (TileRecord) mDetailRecord; 636 tileRecord.openingDetail = false; 637 drawTile(tileRecord, tileRecord.tile.getState()); 638 } 639 } 640 }; 641 642 public interface Callback { 643 void onShowingDetail(QSTile.DetailAdapter detail); 644 void onToggleStateChanged(boolean state); 645 void onScanStateChanged(boolean state); 646 } 647 } 648