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