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.util.TypedValue; 30 import android.view.LayoutInflater; 31 import android.view.View; 32 import android.view.ViewGroup; 33 import android.view.accessibility.AccessibilityEvent; 34 import android.widget.ImageView; 35 import android.widget.TextView; 36 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 private 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 private 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 QSPanel(Context context)83 public QSPanel(Context context) { 84 this(context, null); 85 } 86 QSPanel(Context context, AttributeSet attrs)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 closeDetail(); 115 } 116 }); 117 } 118 updateDetailText()119 private void updateDetailText() { 120 mDetailDoneButton.setText(R.string.quick_settings_done); 121 mDetailSettingsButton.setText(R.string.quick_settings_more_settings); 122 } 123 setBrightnessMirror(BrightnessMirrorController c)124 public void setBrightnessMirror(BrightnessMirrorController c) { 125 super.onFinishInflate(); 126 ToggleSlider brightnessSlider = (ToggleSlider) findViewById(R.id.brightness_slider); 127 ToggleSlider mirror = (ToggleSlider) c.getMirror().findViewById(R.id.brightness_slider); 128 brightnessSlider.setMirror(mirror); 129 brightnessSlider.setMirrorController(c); 130 } 131 setCallback(Callback callback)132 public void setCallback(Callback callback) { 133 mCallback = callback; 134 } 135 setHost(QSTileHost host)136 public void setHost(QSTileHost host) { 137 mHost = host; 138 mFooter.setHost(host); 139 } 140 getHost()141 public QSTileHost getHost() { 142 return mHost; 143 } 144 updateResources()145 public void updateResources() { 146 final Resources res = mContext.getResources(); 147 final int columns = Math.max(1, res.getInteger(R.integer.quick_settings_num_columns)); 148 mCellHeight = res.getDimensionPixelSize(R.dimen.qs_tile_height); 149 mCellWidth = (int)(mCellHeight * TILE_ASPECT); 150 mLargeCellHeight = res.getDimensionPixelSize(R.dimen.qs_dual_tile_height); 151 mLargeCellWidth = (int)(mLargeCellHeight * TILE_ASPECT); 152 mPanelPaddingBottom = res.getDimensionPixelSize(R.dimen.qs_panel_padding_bottom); 153 mDualTileUnderlap = res.getDimensionPixelSize(R.dimen.qs_dual_tile_padding_vertical); 154 mBrightnessPaddingTop = res.getDimensionPixelSize(R.dimen.qs_brightness_padding_top); 155 if (mColumns != columns) { 156 mColumns = columns; 157 postInvalidate(); 158 } 159 if (mListening) { 160 refreshAllTiles(); 161 } 162 updateDetailText(); 163 } 164 165 @Override onConfigurationChanged(Configuration newConfig)166 protected void onConfigurationChanged(Configuration newConfig) { 167 super.onConfigurationChanged(newConfig); 168 FontSizeUtils.updateFontSize(mDetailDoneButton, R.dimen.qs_detail_button_text_size); 169 FontSizeUtils.updateFontSize(mDetailSettingsButton, R.dimen.qs_detail_button_text_size); 170 171 // We need to poke the detail views as well as they might not be attached to the view 172 // hierarchy but reused at a later point. 173 int count = mRecords.size(); 174 for (int i = 0; i < count; i++) { 175 View detailView = mRecords.get(i).detailView; 176 if (detailView != null) { 177 detailView.dispatchConfigurationChanged(newConfig); 178 } 179 } 180 mFooter.onConfigurationChanged(); 181 } 182 setExpanded(boolean expanded)183 public void setExpanded(boolean expanded) { 184 if (mExpanded == expanded) return; 185 mExpanded = expanded; 186 if (!mExpanded) { 187 closeDetail(); 188 } 189 } 190 setListening(boolean listening)191 public void setListening(boolean listening) { 192 if (mListening == listening) return; 193 mListening = listening; 194 for (TileRecord r : mRecords) { 195 r.tile.setListening(mListening); 196 } 197 mFooter.setListening(mListening); 198 if (mListening) { 199 refreshAllTiles(); 200 } 201 if (listening) { 202 mBrightnessController.registerCallbacks(); 203 } else { 204 mBrightnessController.unregisterCallbacks(); 205 } 206 } 207 refreshAllTiles()208 public void refreshAllTiles() { 209 for (TileRecord r : mRecords) { 210 r.tile.refreshState(); 211 } 212 mFooter.refreshState(); 213 } 214 showDetailAdapter(boolean show, DetailAdapter adapter)215 public void showDetailAdapter(boolean show, DetailAdapter adapter) { 216 Record r = new Record(); 217 r.detailAdapter = adapter; 218 showDetail(show, r); 219 } 220 showDetail(boolean show, Record r)221 private void showDetail(boolean show, Record r) { 222 mHandler.obtainMessage(H.SHOW_DETAIL, show ? 1 : 0, 0, r).sendToTarget(); 223 } 224 setTileVisibility(View v, int visibility)225 private void setTileVisibility(View v, int visibility) { 226 mHandler.obtainMessage(H.SET_TILE_VISIBILITY, visibility, 0, v).sendToTarget(); 227 } 228 handleSetTileVisibility(View v, int visibility)229 private void handleSetTileVisibility(View v, int visibility) { 230 if (visibility == v.getVisibility()) return; 231 v.setVisibility(visibility); 232 } 233 setTiles(Collection<QSTile<?>> tiles)234 public void setTiles(Collection<QSTile<?>> tiles) { 235 for (TileRecord record : mRecords) { 236 removeView(record.tileView); 237 } 238 mRecords.clear(); 239 for (QSTile<?> tile : tiles) { 240 addTile(tile); 241 } 242 if (isShowingDetail()) { 243 mDetail.bringToFront(); 244 } 245 } 246 addTile(final QSTile<?> tile)247 private void addTile(final QSTile<?> tile) { 248 final TileRecord r = new TileRecord(); 249 r.tile = tile; 250 r.tileView = tile.createTileView(mContext); 251 r.tileView.setVisibility(View.GONE); 252 final QSTile.Callback callback = new QSTile.Callback() { 253 @Override 254 public void onStateChanged(QSTile.State state) { 255 int visibility = state.visible ? VISIBLE : GONE; 256 if (state.visible && !mGridContentVisible) { 257 258 // We don't want to show it if the content is hidden, 259 // then we just set it to invisible, to ensure that it gets visible again 260 visibility = INVISIBLE; 261 } 262 setTileVisibility(r.tileView, visibility); 263 r.tileView.onStateChanged(state); 264 } 265 @Override 266 public void onShowDetail(boolean show) { 267 QSPanel.this.showDetail(show, r); 268 } 269 @Override 270 public void onToggleStateChanged(boolean state) { 271 if (mDetailRecord == r) { 272 fireToggleStateChanged(state); 273 } 274 } 275 @Override 276 public void onScanStateChanged(boolean state) { 277 r.scanState = state; 278 if (mDetailRecord == r) { 279 fireScanStateChanged(r.scanState); 280 } 281 } 282 283 @Override 284 public void onAnnouncementRequested(CharSequence announcement) { 285 announceForAccessibility(announcement); 286 } 287 }; 288 r.tile.setCallback(callback); 289 final View.OnClickListener click = new View.OnClickListener() { 290 @Override 291 public void onClick(View v) { 292 r.tile.click(); 293 } 294 }; 295 final View.OnClickListener clickSecondary = new View.OnClickListener() { 296 @Override 297 public void onClick(View v) { 298 r.tile.secondaryClick(); 299 } 300 }; 301 final View.OnLongClickListener longClick = new View.OnLongClickListener() { 302 @Override 303 public boolean onLongClick(View v) { 304 r.tile.longClick(); 305 return true; 306 } 307 }; 308 r.tileView.init(click, clickSecondary, longClick); 309 r.tile.setListening(mListening); 310 callback.onStateChanged(r.tile.getState()); 311 r.tile.refreshState(); 312 mRecords.add(r); 313 314 addView(r.tileView); 315 } 316 isShowingDetail()317 public boolean isShowingDetail() { 318 return mDetailRecord != null; 319 } 320 closeDetail()321 public void closeDetail() { 322 showDetail(false, mDetailRecord); 323 } 324 isClosingDetail()325 public boolean isClosingDetail() { 326 return mClosingDetail; 327 } 328 getGridHeight()329 public int getGridHeight() { 330 return mGridHeight; 331 } 332 handleShowDetail(Record r, boolean show)333 private void handleShowDetail(Record r, boolean show) { 334 if (r instanceof TileRecord) { 335 handleShowDetailTile((TileRecord) r, show); 336 } else { 337 handleShowDetailImpl(r, show, getWidth() /* x */, 0/* y */); 338 } 339 } 340 handleShowDetailTile(TileRecord r, boolean show)341 private void handleShowDetailTile(TileRecord r, boolean show) { 342 if ((mDetailRecord != null) == show) return; 343 344 if (show) { 345 r.detailAdapter = r.tile.getDetailAdapter(); 346 if (r.detailAdapter == null) return; 347 } 348 int x = r.tileView.getLeft() + r.tileView.getWidth() / 2; 349 int y = r.tileView.getTop() + r.tileView.getHeight() / 2; 350 handleShowDetailImpl(r, show, x, y); 351 } 352 handleShowDetailImpl(Record r, boolean show, int x, int y)353 private void handleShowDetailImpl(Record r, boolean show, int x, int y) { 354 if ((mDetailRecord != null) == show) return; // already in right state 355 DetailAdapter detailAdapter = null; 356 AnimatorListener listener = null; 357 if (show) { 358 detailAdapter = r.detailAdapter; 359 r.detailView = detailAdapter.createDetailView(mContext, r.detailView, mDetailContent); 360 if (r.detailView == null) throw new IllegalStateException("Must return detail view"); 361 362 final Intent settingsIntent = detailAdapter.getSettingsIntent(); 363 mDetailSettingsButton.setVisibility(settingsIntent != null ? VISIBLE : GONE); 364 mDetailSettingsButton.setOnClickListener(new OnClickListener() { 365 @Override 366 public void onClick(View v) { 367 mHost.startSettingsActivity(settingsIntent); 368 } 369 }); 370 371 mDetailContent.removeAllViews(); 372 mDetail.bringToFront(); 373 mDetailContent.addView(r.detailView); 374 setDetailRecord(r); 375 listener = mHideGridContentWhenDone; 376 } else { 377 mClosingDetail = true; 378 setGridContentVisibility(true); 379 listener = mTeardownDetailWhenDone; 380 fireScanStateChanged(false); 381 } 382 sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); 383 fireShowingDetail(show ? detailAdapter : null); 384 mClipper.animateCircularClip(x, y, show, listener); 385 } 386 setGridContentVisibility(boolean visible)387 private void setGridContentVisibility(boolean visible) { 388 int newVis = visible ? VISIBLE : INVISIBLE; 389 for (int i = 0; i < mRecords.size(); i++) { 390 TileRecord tileRecord = mRecords.get(i); 391 if (tileRecord.tileView.getVisibility() != GONE) { 392 tileRecord.tileView.setVisibility(newVis); 393 } 394 } 395 mBrightnessView.setVisibility(newVis); 396 mGridContentVisible = visible; 397 } 398 399 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)400 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 401 final int width = MeasureSpec.getSize(widthMeasureSpec); 402 mBrightnessView.measure(exactly(width), MeasureSpec.UNSPECIFIED); 403 final int brightnessHeight = mBrightnessView.getMeasuredHeight() + mBrightnessPaddingTop; 404 mFooter.getView().measure(exactly(width), MeasureSpec.UNSPECIFIED); 405 int r = -1; 406 int c = -1; 407 int rows = 0; 408 boolean rowIsDual = false; 409 for (TileRecord record : mRecords) { 410 if (record.tileView.getVisibility() == GONE) continue; 411 // wrap to next column if we've reached the max # of columns 412 // also don't allow dual + single tiles on the same row 413 if (r == -1 || c == (mColumns - 1) || rowIsDual != record.tile.supportsDualTargets()) { 414 r++; 415 c = 0; 416 rowIsDual = record.tile.supportsDualTargets(); 417 } else { 418 c++; 419 } 420 record.row = r; 421 record.col = c; 422 rows = r + 1; 423 } 424 425 for (TileRecord record : mRecords) { 426 if (record.tileView.setDual(record.tile.supportsDualTargets())) { 427 record.tileView.handleStateChanged(record.tile.getState()); 428 } 429 if (record.tileView.getVisibility() == GONE) continue; 430 final int cw = record.row == 0 ? mLargeCellWidth : mCellWidth; 431 final int ch = record.row == 0 ? mLargeCellHeight : mCellHeight; 432 record.tileView.measure(exactly(cw), exactly(ch)); 433 } 434 int h = rows == 0 ? brightnessHeight : (getRowTop(rows) + mPanelPaddingBottom); 435 if (mFooter.hasFooter()) { 436 h += mFooter.getView().getMeasuredHeight(); 437 } 438 mDetail.measure(exactly(width), MeasureSpec.UNSPECIFIED); 439 if (mDetail.getMeasuredHeight() < h) { 440 mDetail.measure(exactly(width), exactly(h)); 441 } 442 mGridHeight = h; 443 setMeasuredDimension(width, Math.max(h, mDetail.getMeasuredHeight())); 444 } 445 exactly(int size)446 private static int exactly(int size) { 447 return MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY); 448 } 449 450 @Override onLayout(boolean changed, int l, int t, int r, int b)451 protected void onLayout(boolean changed, int l, int t, int r, int b) { 452 final int w = getWidth(); 453 mBrightnessView.layout(0, mBrightnessPaddingTop, 454 mBrightnessView.getMeasuredWidth(), 455 mBrightnessPaddingTop + mBrightnessView.getMeasuredHeight()); 456 boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL; 457 for (TileRecord record : mRecords) { 458 if (record.tileView.getVisibility() == GONE) continue; 459 final int cols = getColumnCount(record.row); 460 final int cw = record.row == 0 ? mLargeCellWidth : mCellWidth; 461 final int extra = (w - cw * cols) / (cols + 1); 462 int left = record.col * cw + (record.col + 1) * extra; 463 final int top = getRowTop(record.row); 464 int right; 465 int tileWith = record.tileView.getMeasuredWidth(); 466 if (isRtl) { 467 right = w - left; 468 left = right - tileWith; 469 } else { 470 right = left + tileWith; 471 } 472 record.tileView.layout(left, top, right, top + record.tileView.getMeasuredHeight()); 473 } 474 final int dh = Math.max(mDetail.getMeasuredHeight(), getMeasuredHeight()); 475 mDetail.layout(0, 0, mDetail.getMeasuredWidth(), dh); 476 if (mFooter.hasFooter()) { 477 View footer = mFooter.getView(); 478 footer.layout(0, getMeasuredHeight() - footer.getMeasuredHeight(), 479 footer.getMeasuredWidth(), getMeasuredHeight()); 480 } 481 } 482 getRowTop(int row)483 private int getRowTop(int row) { 484 if (row <= 0) return mBrightnessView.getMeasuredHeight() + mBrightnessPaddingTop; 485 return mBrightnessView.getMeasuredHeight() + mBrightnessPaddingTop 486 + mLargeCellHeight - mDualTileUnderlap + (row - 1) * mCellHeight; 487 } 488 getColumnCount(int row)489 private int getColumnCount(int row) { 490 int cols = 0; 491 for (TileRecord record : mRecords) { 492 if (record.tileView.getVisibility() == GONE) continue; 493 if (record.row == row) cols++; 494 } 495 return cols; 496 } 497 fireShowingDetail(QSTile.DetailAdapter detail)498 private void fireShowingDetail(QSTile.DetailAdapter detail) { 499 if (mCallback != null) { 500 mCallback.onShowingDetail(detail); 501 } 502 } 503 fireToggleStateChanged(boolean state)504 private void fireToggleStateChanged(boolean state) { 505 if (mCallback != null) { 506 mCallback.onToggleStateChanged(state); 507 } 508 } 509 fireScanStateChanged(boolean state)510 private void fireScanStateChanged(boolean state) { 511 if (mCallback != null) { 512 mCallback.onScanStateChanged(state); 513 } 514 } 515 setDetailRecord(Record r)516 private void setDetailRecord(Record r) { 517 if (r == mDetailRecord) return; 518 mDetailRecord = r; 519 final boolean scanState = mDetailRecord instanceof TileRecord 520 && ((TileRecord) mDetailRecord).scanState; 521 fireScanStateChanged(scanState); 522 } 523 524 private class H extends Handler { 525 private static final int SHOW_DETAIL = 1; 526 private static final int SET_TILE_VISIBILITY = 2; 527 @Override handleMessage(Message msg)528 public void handleMessage(Message msg) { 529 if (msg.what == SHOW_DETAIL) { 530 handleShowDetail((Record)msg.obj, msg.arg1 != 0); 531 } else if (msg.what == SET_TILE_VISIBILITY) { 532 handleSetTileVisibility((View)msg.obj, msg.arg1); 533 } 534 } 535 } 536 537 private static class Record { 538 View detailView; 539 DetailAdapter detailAdapter; 540 } 541 542 private static final class TileRecord extends Record { 543 QSTile<?> tile; 544 QSTileView tileView; 545 int row; 546 int col; 547 boolean scanState; 548 } 549 550 private final AnimatorListenerAdapter mTeardownDetailWhenDone = new AnimatorListenerAdapter() { 551 public void onAnimationEnd(Animator animation) { 552 mDetailContent.removeAllViews(); 553 setDetailRecord(null); 554 mClosingDetail = false; 555 }; 556 }; 557 558 private final AnimatorListenerAdapter mHideGridContentWhenDone = new AnimatorListenerAdapter() { 559 public void onAnimationCancel(Animator animation) { 560 // If we have been cancelled, remove the listener so that onAnimationEnd doesn't get 561 // called, this will avoid accidentally turning off the grid when we don't want to. 562 animation.removeListener(this); 563 }; 564 565 @Override 566 public void onAnimationEnd(Animator animation) { 567 // Only hide content if still in detail state. 568 if (mDetailRecord != null) { 569 setGridContentVisibility(false); 570 } 571 } 572 }; 573 574 public interface Callback { onShowingDetail(QSTile.DetailAdapter detail)575 void onShowingDetail(QSTile.DetailAdapter detail); onToggleStateChanged(boolean state)576 void onToggleStateChanged(boolean state); onScanStateChanged(boolean state)577 void onScanStateChanged(boolean state); 578 } 579 } 580