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 static com.android.systemui.qs.tileimpl.QSTileImpl.getColorForState; 20 21 import android.content.ComponentName; 22 import android.content.Context; 23 import android.content.res.Configuration; 24 import android.content.res.Resources; 25 import android.metrics.LogMaker; 26 import android.os.Handler; 27 import android.os.Message; 28 import android.service.quicksettings.Tile; 29 import android.util.AttributeSet; 30 import android.view.LayoutInflater; 31 import android.view.View; 32 import android.widget.ImageView; 33 import android.widget.LinearLayout; 34 35 import com.android.internal.logging.MetricsLogger; 36 import com.android.internal.logging.nano.MetricsProto.MetricsEvent; 37 import com.android.settingslib.Utils; 38 import com.android.systemui.Dependency; 39 import com.android.systemui.R; 40 import com.android.systemui.plugins.qs.DetailAdapter; 41 import com.android.systemui.plugins.qs.QSTile; 42 import com.android.systemui.plugins.qs.QSTileView; 43 import com.android.systemui.qs.QSHost.Callback; 44 import com.android.systemui.qs.customize.QSCustomizer; 45 import com.android.systemui.qs.external.CustomTile; 46 import com.android.systemui.settings.BrightnessController; 47 import com.android.systemui.settings.ToggleSliderView; 48 import com.android.systemui.statusbar.policy.BrightnessMirrorController; 49 import com.android.systemui.statusbar.policy.BrightnessMirrorController.BrightnessMirrorListener; 50 import com.android.systemui.tuner.TunerService; 51 import com.android.systemui.tuner.TunerService.Tunable; 52 53 import java.util.ArrayList; 54 import java.util.Collection; 55 56 /** View that represents the quick settings tile panel. **/ 57 public class QSPanel extends LinearLayout implements Tunable, Callback, BrightnessMirrorListener { 58 59 public static final String QS_SHOW_BRIGHTNESS = "qs_show_brightness"; 60 61 protected final Context mContext; 62 protected final ArrayList<TileRecord> mRecords = new ArrayList<TileRecord>(); 63 protected final View mBrightnessView; 64 private final H mHandler = new H(); 65 private final View mPageIndicator; 66 private final MetricsLogger mMetricsLogger = Dependency.get(MetricsLogger.class); 67 68 private int mPanelPaddingBottom; 69 private int mBrightnessPaddingTop; 70 protected boolean mExpanded; 71 protected boolean mListening; 72 73 private QSDetail.Callback mCallback; 74 private BrightnessController mBrightnessController; 75 protected QSTileHost mHost; 76 77 protected QSSecurityFooter mFooter; 78 private boolean mGridContentVisible = true; 79 80 protected QSTileLayout mTileLayout; 81 82 private QSCustomizer mCustomizePanel; 83 private Record mDetailRecord; 84 85 private BrightnessMirrorController mBrightnessMirrorController; 86 private View mDivider; 87 88 public QSPanel(Context context) { 89 this(context, null); 90 } 91 92 public QSPanel(Context context, AttributeSet attrs) { 93 super(context, attrs); 94 mContext = context; 95 96 setOrientation(VERTICAL); 97 98 mBrightnessView = LayoutInflater.from(context).inflate( 99 R.layout.quick_settings_brightness_dialog, this, false); 100 addView(mBrightnessView); 101 102 setupTileLayout(); 103 104 mPageIndicator = LayoutInflater.from(context).inflate( 105 R.layout.qs_page_indicator, this, false); 106 addView(mPageIndicator); 107 if (mTileLayout instanceof PagedTileLayout) { 108 ((PagedTileLayout) mTileLayout).setPageIndicator((PageIndicator) mPageIndicator); 109 } 110 111 addDivider(); 112 113 mFooter = new QSSecurityFooter(this, context); 114 addView(mFooter.getView()); 115 116 updateResources(); 117 118 mBrightnessController = new BrightnessController(getContext(), 119 findViewById(R.id.brightness_icon), 120 findViewById(R.id.brightness_slider)); 121 } 122 123 protected void addDivider() { 124 mDivider = LayoutInflater.from(mContext).inflate(R.layout.qs_divider, this, false); 125 mDivider.setBackgroundColor(Utils.applyAlpha(mDivider.getAlpha(), 126 getColorForState(mContext, Tile.STATE_ACTIVE))); 127 addView(mDivider); 128 } 129 130 public View getDivider() { 131 return mDivider; 132 } 133 134 public View getPageIndicator() { 135 return mPageIndicator; 136 } 137 138 protected void setupTileLayout() { 139 mTileLayout = (QSTileLayout) LayoutInflater.from(mContext).inflate( 140 R.layout.qs_paged_tile_layout, this, false); 141 mTileLayout.setListening(mListening); 142 addView((View) mTileLayout); 143 } 144 145 public boolean isShowingCustomize() { 146 return mCustomizePanel != null && mCustomizePanel.isCustomizing(); 147 } 148 149 @Override 150 protected void onAttachedToWindow() { 151 super.onAttachedToWindow(); 152 Dependency.get(TunerService.class).addTunable(this, QS_SHOW_BRIGHTNESS); 153 if (mHost != null) { 154 setTiles(mHost.getTiles()); 155 } 156 if (mBrightnessMirrorController != null) { 157 mBrightnessMirrorController.addCallback(this); 158 } 159 } 160 161 @Override 162 protected void onDetachedFromWindow() { 163 Dependency.get(TunerService.class).removeTunable(this); 164 if (mHost != null) { 165 mHost.removeCallback(this); 166 } 167 for (TileRecord record : mRecords) { 168 record.tile.removeCallbacks(); 169 } 170 if (mBrightnessMirrorController != null) { 171 mBrightnessMirrorController.removeCallback(this); 172 } 173 super.onDetachedFromWindow(); 174 } 175 176 @Override 177 public void onTilesChanged() { 178 setTiles(mHost.getTiles()); 179 } 180 181 @Override 182 public void onTuningChanged(String key, String newValue) { 183 if (QS_SHOW_BRIGHTNESS.equals(key)) { 184 mBrightnessView.setVisibility(newValue == null || Integer.parseInt(newValue) != 0 185 ? VISIBLE : GONE); 186 } 187 } 188 189 public void openDetails(String subPanel) { 190 QSTile tile = getTile(subPanel); 191 showDetailAdapter(true, tile.getDetailAdapter(), new int[]{getWidth() / 2, 0}); 192 } 193 194 private QSTile getTile(String subPanel) { 195 for (int i = 0; i < mRecords.size(); i++) { 196 if (subPanel.equals(mRecords.get(i).tile.getTileSpec())) { 197 return mRecords.get(i).tile; 198 } 199 } 200 return mHost.createTile(subPanel); 201 } 202 203 public void setBrightnessMirror(BrightnessMirrorController c) { 204 if (mBrightnessMirrorController != null) { 205 mBrightnessMirrorController.removeCallback(this); 206 } 207 mBrightnessMirrorController = c; 208 if (mBrightnessMirrorController != null) { 209 mBrightnessMirrorController.addCallback(this); 210 } 211 updateBrightnessMirror(); 212 } 213 214 @Override 215 public void onBrightnessMirrorReinflated(View brightnessMirror) { 216 updateBrightnessMirror(); 217 } 218 219 View getBrightnessView() { 220 return mBrightnessView; 221 } 222 223 public void setCallback(QSDetail.Callback callback) { 224 mCallback = callback; 225 } 226 227 public void setHost(QSTileHost host, QSCustomizer customizer) { 228 mHost = host; 229 mHost.addCallback(this); 230 setTiles(mHost.getTiles()); 231 mFooter.setHostEnvironment(host); 232 mCustomizePanel = customizer; 233 if (mCustomizePanel != null) { 234 mCustomizePanel.setHost(mHost); 235 } 236 } 237 238 public QSTileHost getHost() { 239 return mHost; 240 } 241 242 public void updateResources() { 243 final Resources res = mContext.getResources(); 244 mPanelPaddingBottom = res.getDimensionPixelSize(R.dimen.qs_panel_padding_bottom); 245 mBrightnessPaddingTop = res.getDimensionPixelSize(R.dimen.qs_brightness_padding_top); 246 setPadding(0, mBrightnessPaddingTop, 0, mPanelPaddingBottom); 247 for (TileRecord r : mRecords) { 248 r.tile.clearState(); 249 } 250 if (mListening) { 251 refreshAllTiles(); 252 } 253 if (mTileLayout != null) { 254 mTileLayout.updateResources(); 255 } 256 } 257 258 @Override 259 protected void onConfigurationChanged(Configuration newConfig) { 260 super.onConfigurationChanged(newConfig); 261 mFooter.onConfigurationChanged(); 262 263 updateBrightnessMirror(); 264 } 265 266 public void updateBrightnessMirror() { 267 if (mBrightnessMirrorController != null) { 268 ToggleSliderView brightnessSlider = findViewById(R.id.brightness_slider); 269 ToggleSliderView mirrorSlider = mBrightnessMirrorController.getMirror() 270 .findViewById(R.id.brightness_slider); 271 brightnessSlider.setMirror(mirrorSlider); 272 brightnessSlider.setMirrorController(mBrightnessMirrorController); 273 } 274 } 275 276 public void onCollapse() { 277 if (mCustomizePanel != null && mCustomizePanel.isShown()) { 278 mCustomizePanel.hide(mCustomizePanel.getWidth() / 2, mCustomizePanel.getHeight() / 2); 279 } 280 } 281 282 public void setExpanded(boolean expanded) { 283 if (mExpanded == expanded) return; 284 mExpanded = expanded; 285 if (!mExpanded && mTileLayout instanceof PagedTileLayout) { 286 ((PagedTileLayout) mTileLayout).setCurrentItem(0, false); 287 } 288 mMetricsLogger.visibility(MetricsEvent.QS_PANEL, mExpanded); 289 if (!mExpanded) { 290 closeDetail(); 291 } else { 292 logTiles(); 293 } 294 } 295 296 public boolean isExpanded() { 297 return mExpanded; 298 } 299 300 public void setListening(boolean listening) { 301 if (mListening == listening) return; 302 mListening = listening; 303 if (mTileLayout != null) { 304 mTileLayout.setListening(listening); 305 } 306 mFooter.setListening(mListening); 307 if (mListening) { 308 refreshAllTiles(); 309 } 310 if (mBrightnessView.getVisibility() == View.VISIBLE) { 311 if (listening) { 312 mBrightnessController.registerCallbacks(); 313 } else { 314 mBrightnessController.unregisterCallbacks(); 315 } 316 } 317 } 318 319 public void refreshAllTiles() { 320 for (TileRecord r : mRecords) { 321 r.tile.refreshState(); 322 } 323 mFooter.refreshState(); 324 } 325 326 public void showDetailAdapter(boolean show, DetailAdapter adapter, int[] locationInWindow) { 327 int xInWindow = locationInWindow[0]; 328 int yInWindow = locationInWindow[1]; 329 ((View) getParent()).getLocationInWindow(locationInWindow); 330 331 Record r = new Record(); 332 r.detailAdapter = adapter; 333 r.x = xInWindow - locationInWindow[0]; 334 r.y = yInWindow - locationInWindow[1]; 335 336 locationInWindow[0] = xInWindow; 337 locationInWindow[1] = yInWindow; 338 339 showDetail(show, r); 340 } 341 342 protected void showDetail(boolean show, Record r) { 343 mHandler.obtainMessage(H.SHOW_DETAIL, show ? 1 : 0, 0, r).sendToTarget(); 344 } 345 346 public void setTiles(Collection<QSTile> tiles) { 347 setTiles(tiles, false); 348 } 349 350 public void setTiles(Collection<QSTile> tiles, boolean collapsedView) { 351 for (TileRecord record : mRecords) { 352 mTileLayout.removeTile(record); 353 record.tile.removeCallback(record.callback); 354 } 355 mRecords.clear(); 356 for (QSTile tile : tiles) { 357 addTile(tile, collapsedView); 358 } 359 } 360 361 protected void drawTile(TileRecord r, QSTile.State state) { 362 r.tileView.onStateChanged(state); 363 } 364 365 protected QSTileView createTileView(QSTile tile, boolean collapsedView) { 366 return mHost.createTileView(tile, collapsedView); 367 } 368 369 protected boolean shouldShowDetail() { 370 return mExpanded; 371 } 372 373 protected TileRecord addTile(final QSTile tile, boolean collapsedView) { 374 final TileRecord r = new TileRecord(); 375 r.tile = tile; 376 r.tileView = createTileView(tile, collapsedView); 377 final QSTile.Callback callback = new QSTile.Callback() { 378 @Override 379 public void onStateChanged(QSTile.State state) { 380 drawTile(r, state); 381 } 382 383 @Override 384 public void onShowDetail(boolean show) { 385 // Both the collapsed and full QS panels get this callback, this check determines 386 // which one should handle showing the detail. 387 if (shouldShowDetail()) { 388 QSPanel.this.showDetail(show, r); 389 } 390 } 391 392 @Override 393 public void onToggleStateChanged(boolean state) { 394 if (mDetailRecord == r) { 395 fireToggleStateChanged(state); 396 } 397 } 398 399 @Override 400 public void onScanStateChanged(boolean state) { 401 r.scanState = state; 402 if (mDetailRecord == r) { 403 fireScanStateChanged(r.scanState); 404 } 405 } 406 407 @Override 408 public void onAnnouncementRequested(CharSequence announcement) { 409 if (announcement != null) { 410 mHandler.obtainMessage(H.ANNOUNCE_FOR_ACCESSIBILITY, announcement) 411 .sendToTarget(); 412 } 413 } 414 }; 415 r.tile.addCallback(callback); 416 r.callback = callback; 417 r.tileView.init(r.tile); 418 r.tile.refreshState(); 419 mRecords.add(r); 420 421 if (mTileLayout != null) { 422 mTileLayout.addTile(r); 423 } 424 425 return r; 426 } 427 428 429 public void showEdit(final View v) { 430 v.post(new Runnable() { 431 @Override 432 public void run() { 433 if (mCustomizePanel != null) { 434 if (!mCustomizePanel.isCustomizing()) { 435 int[] loc = new int[2]; 436 v.getLocationInWindow(loc); 437 int x = loc[0] + v.getWidth() / 2; 438 int y = loc[1] + v.getHeight() / 2; 439 mCustomizePanel.show(x, y); 440 } 441 } 442 443 } 444 }); 445 } 446 447 public void closeDetail() { 448 if (mCustomizePanel != null && mCustomizePanel.isShown()) { 449 // Treat this as a detail panel for now, to make things easy. 450 mCustomizePanel.hide(mCustomizePanel.getWidth() / 2, mCustomizePanel.getHeight() / 2); 451 return; 452 } 453 showDetail(false, mDetailRecord); 454 } 455 456 public int getGridHeight() { 457 return getMeasuredHeight(); 458 } 459 460 protected void handleShowDetail(Record r, boolean show) { 461 if (r instanceof TileRecord) { 462 handleShowDetailTile((TileRecord) r, show); 463 } else { 464 int x = 0; 465 int y = 0; 466 if (r != null) { 467 x = r.x; 468 y = r.y; 469 } 470 handleShowDetailImpl(r, show, x, y); 471 } 472 } 473 474 private void handleShowDetailTile(TileRecord r, boolean show) { 475 if ((mDetailRecord != null) == show && mDetailRecord == r) return; 476 477 if (show) { 478 r.detailAdapter = r.tile.getDetailAdapter(); 479 if (r.detailAdapter == null) return; 480 } 481 r.tile.setDetailListening(show); 482 int x = r.tileView.getLeft() + r.tileView.getWidth() / 2; 483 int y = r.tileView.getDetailY() + mTileLayout.getOffsetTop(r) + getTop(); 484 handleShowDetailImpl(r, show, x, y); 485 } 486 487 private void handleShowDetailImpl(Record r, boolean show, int x, int y) { 488 setDetailRecord(show ? r : null); 489 fireShowingDetail(show ? r.detailAdapter : null, x, y); 490 } 491 492 protected void setDetailRecord(Record r) { 493 if (r == mDetailRecord) return; 494 mDetailRecord = r; 495 final boolean scanState = mDetailRecord instanceof TileRecord 496 && ((TileRecord) mDetailRecord).scanState; 497 fireScanStateChanged(scanState); 498 } 499 500 void setGridContentVisibility(boolean visible) { 501 int newVis = visible ? VISIBLE : INVISIBLE; 502 setVisibility(newVis); 503 if (mGridContentVisible != visible) { 504 mMetricsLogger.visibility(MetricsEvent.QS_PANEL, newVis); 505 } 506 mGridContentVisible = visible; 507 } 508 509 private void logTiles() { 510 for (int i = 0; i < mRecords.size(); i++) { 511 QSTile tile = mRecords.get(i).tile; 512 mMetricsLogger.write(tile.populate(new LogMaker(tile.getMetricsCategory()) 513 .setType(MetricsEvent.TYPE_OPEN))); 514 } 515 } 516 517 private void fireShowingDetail(DetailAdapter detail, int x, int y) { 518 if (mCallback != null) { 519 mCallback.onShowingDetail(detail, x, y); 520 } 521 } 522 523 private void fireToggleStateChanged(boolean state) { 524 if (mCallback != null) { 525 mCallback.onToggleStateChanged(state); 526 } 527 } 528 529 private void fireScanStateChanged(boolean state) { 530 if (mCallback != null) { 531 mCallback.onScanStateChanged(state); 532 } 533 } 534 535 public void clickTile(ComponentName tile) { 536 final String spec = CustomTile.toSpec(tile); 537 final int N = mRecords.size(); 538 for (int i = 0; i < N; i++) { 539 if (mRecords.get(i).tile.getTileSpec().equals(spec)) { 540 mRecords.get(i).tile.click(); 541 break; 542 } 543 } 544 } 545 546 QSTileLayout getTileLayout() { 547 return mTileLayout; 548 } 549 550 QSTileView getTileView(QSTile tile) { 551 for (TileRecord r : mRecords) { 552 if (r.tile == tile) { 553 return r.tileView; 554 } 555 } 556 return null; 557 } 558 559 public QSSecurityFooter getFooter() { 560 return mFooter; 561 } 562 563 public void showDeviceMonitoringDialog() { 564 mFooter.showDeviceMonitoringDialog(); 565 } 566 567 private class H extends Handler { 568 private static final int SHOW_DETAIL = 1; 569 private static final int SET_TILE_VISIBILITY = 2; 570 private static final int ANNOUNCE_FOR_ACCESSIBILITY = 3; 571 572 @Override 573 public void handleMessage(Message msg) { 574 if (msg.what == SHOW_DETAIL) { 575 handleShowDetail((Record) msg.obj, msg.arg1 != 0); 576 } else if (msg.what == ANNOUNCE_FOR_ACCESSIBILITY) { 577 announceForAccessibility((CharSequence) msg.obj); 578 } 579 } 580 } 581 582 protected static class Record { 583 DetailAdapter detailAdapter; 584 int x; 585 int y; 586 } 587 588 public static final class TileRecord extends Record { 589 public QSTile tile; 590 public com.android.systemui.plugins.qs.QSTileView tileView; 591 public boolean scanState; 592 public QSTile.Callback callback; 593 } 594 595 public interface QSTileLayout { 596 void addTile(TileRecord tile); 597 598 void removeTile(TileRecord tile); 599 600 int getOffsetTop(TileRecord tile); 601 602 boolean updateResources(); 603 604 void setListening(boolean listening); 605 } 606 } 607