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