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