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