1 /* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 5 * except in compliance with the License. You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software distributed under the 10 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 11 * KIND, either express or implied. See the License for the specific language governing 12 * permissions and limitations under the License. 13 */ 14 15 package com.android.systemui.qs.tileimpl; 16 17 import static androidx.lifecycle.Lifecycle.State.CREATED; 18 import static androidx.lifecycle.Lifecycle.State.DESTROYED; 19 import static androidx.lifecycle.Lifecycle.State.RESUMED; 20 import static androidx.lifecycle.Lifecycle.State.STARTED; 21 22 import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.ACTION_QS_CLICK; 23 import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.ACTION_QS_LONG_PRESS; 24 import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.ACTION_QS_SECONDARY_CLICK; 25 import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.FIELD_IS_FULL_QS; 26 import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.FIELD_QS_POSITION; 27 import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.FIELD_QS_VALUE; 28 import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.FIELD_STATUS_BAR_STATE; 29 import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.TYPE_ACTION; 30 import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; 31 32 import android.annotation.CallSuper; 33 import android.annotation.NonNull; 34 import android.content.Context; 35 import android.content.Intent; 36 import android.graphics.drawable.Drawable; 37 import android.metrics.LogMaker; 38 import android.os.Handler; 39 import android.os.Looper; 40 import android.os.Message; 41 import android.text.format.DateUtils; 42 import android.util.ArraySet; 43 import android.util.Log; 44 import android.util.SparseArray; 45 46 import androidx.annotation.Nullable; 47 import androidx.lifecycle.Lifecycle; 48 import androidx.lifecycle.LifecycleOwner; 49 import androidx.lifecycle.LifecycleRegistry; 50 51 import com.android.internal.annotations.VisibleForTesting; 52 import com.android.internal.jank.InteractionJankMonitor; 53 import com.android.internal.logging.InstanceId; 54 import com.android.internal.logging.MetricsLogger; 55 import com.android.internal.logging.UiEventLogger; 56 import com.android.settingslib.RestrictedLockUtils; 57 import com.android.settingslib.RestrictedLockUtilsInternal; 58 import com.android.settingslib.graph.SignalDrawable; 59 import com.android.systemui.Dumpable; 60 import com.android.systemui.animation.ActivityTransitionAnimator; 61 import com.android.systemui.animation.Expandable; 62 import com.android.systemui.plugins.ActivityStarter; 63 import com.android.systemui.plugins.FalsingManager; 64 import com.android.systemui.plugins.qs.QSTile; 65 import com.android.systemui.plugins.qs.QSTile.State; 66 import com.android.systemui.plugins.statusbar.StatusBarStateController; 67 import com.android.systemui.qs.QSEvent; 68 import com.android.systemui.qs.QSHost; 69 import com.android.systemui.qs.QsEventLogger; 70 import com.android.systemui.qs.SideLabelTileLayout; 71 import com.android.systemui.qs.logging.QSLogger; 72 73 import java.io.PrintWriter; 74 75 /** 76 * Base quick-settings tile, extend this to create a new tile. 77 * 78 * State management done on a looper provided by the host. Tiles should update state in 79 * handleUpdateState. Callbacks affecting state should use refreshState to trigger another 80 * state update pass on tile looper. 81 * 82 * @param <TState> see above 83 */ 84 public abstract class QSTileImpl<TState extends State> implements QSTile, LifecycleOwner, Dumpable { 85 protected final String TAG = "Tile." + getClass().getSimpleName(); 86 protected final boolean DEBUG = Log.isLoggable("Tile", Log.DEBUG); 87 88 private static final long DEFAULT_STALE_TIMEOUT = 10 * DateUtils.MINUTE_IN_MILLIS; 89 protected static final Object ARG_SHOW_TRANSIENT_ENABLING = new Object(); 90 91 private static final int READY_STATE_NOT_READY = 0; 92 private static final int READY_STATE_READYING = 1; 93 private static final int READY_STATE_READY = 2; 94 95 protected final QSHost mHost; 96 protected final Context mContext; 97 // @NonFinalForTesting 98 protected final H mHandler; 99 protected final Handler mUiHandler; 100 private final ArraySet<Object> mListeners = new ArraySet<>(); 101 private final MetricsLogger mMetricsLogger; 102 private final StatusBarStateController mStatusBarStateController; 103 protected final ActivityStarter mActivityStarter; 104 private final UiEventLogger mUiEventLogger; 105 private final FalsingManager mFalsingManager; 106 protected final QSLogger mQSLogger; 107 private volatile int mReadyState; 108 // Keeps track of the click event, to match it with the handling in the background thread 109 // Only read and modified in main thread (where click events come through). 110 private int mClickEventId = 0; 111 112 private final ArraySet<Callback> mCallbacks = new ArraySet<>(); 113 private final Object mStaleListener = new Object(); 114 protected TState mState; 115 private TState mTmpState; 116 private final InstanceId mInstanceId; 117 private boolean mAnnounceNextStateChange; 118 119 private String mTileSpec; 120 @Nullable 121 @VisibleForTesting 122 protected EnforcedAdmin mEnforcedAdmin; 123 private boolean mShowingDetail; 124 private int mIsFullQs; 125 126 private final LifecycleRegistry mLifecycle = new LifecycleRegistry(this); 127 128 /** 129 * Provides a new {@link TState} of the appropriate type to use between this tile and the 130 * corresponding view. 131 * 132 * @return new state to use by the tile. 133 */ newTileState()134 public abstract TState newTileState(); 135 136 /** 137 * Handles clicks by the user. 138 * 139 * Calls to the controller should be made here to set the new state of the device. 140 * 141 * @param expandable {@link Expandable} that was clicked. 142 */ handleClick(@ullable Expandable expandable)143 protected abstract void handleClick(@Nullable Expandable expandable); 144 145 /** 146 * Update state of the tile based on device state 147 * 148 * Called whenever the state of the tile needs to be updated, either after user 149 * interaction or from callbacks from the controller. It populates {@code state} with the 150 * information to display to the user. 151 * 152 * @param state {@link TState} to populate with information to display 153 * @param arg additional arguments needed to populate {@code state} 154 */ handleUpdateState(TState state, Object arg)155 abstract protected void handleUpdateState(TState state, Object arg); 156 157 /** 158 * Declare the category of this tile. 159 * 160 * Categories are defined in {@link com.android.internal.logging.nano.MetricsProto.MetricsEvent} 161 * by editing frameworks/base/proto/src/metrics_constants.proto. 162 * 163 * @deprecated Not needed as this logging is deprecated. Logging tiles is done using 164 * {@link QSTile#getMetricsSpec} 165 */ 166 @Deprecated getMetricsCategory()167 public int getMetricsCategory() { 168 return 0; 169 } 170 171 /** 172 * Performs initialization of the tile 173 * 174 * Use this to perform initialization of the tile. Empty by default. 175 */ handleInitialize()176 protected void handleInitialize() { 177 178 } 179 QSTileImpl( QSHost host, QsEventLogger uiEventLogger, Looper backgroundLooper, Handler mainHandler, FalsingManager falsingManager, MetricsLogger metricsLogger, StatusBarStateController statusBarStateController, ActivityStarter activityStarter, QSLogger qsLogger )180 protected QSTileImpl( 181 QSHost host, 182 QsEventLogger uiEventLogger, 183 Looper backgroundLooper, 184 Handler mainHandler, 185 FalsingManager falsingManager, 186 MetricsLogger metricsLogger, 187 StatusBarStateController statusBarStateController, 188 ActivityStarter activityStarter, 189 QSLogger qsLogger 190 ) { 191 mHost = host; 192 mContext = host.getContext(); 193 mInstanceId = uiEventLogger.getNewInstanceId(); 194 mUiEventLogger = uiEventLogger; 195 196 mUiHandler = mainHandler; 197 mHandler = new H(backgroundLooper); 198 mFalsingManager = falsingManager; 199 mQSLogger = qsLogger; 200 mMetricsLogger = metricsLogger; 201 mStatusBarStateController = statusBarStateController; 202 mActivityStarter = activityStarter; 203 204 resetStates(); 205 mUiHandler.post(() -> mLifecycle.setCurrentState(CREATED)); 206 } 207 resetStates()208 protected final void resetStates() { 209 mState = newTileState(); 210 mTmpState = newTileState(); 211 mState.spec = mTileSpec; 212 mTmpState.spec = mTileSpec; 213 } 214 215 @NonNull 216 @Override getLifecycle()217 public Lifecycle getLifecycle() { 218 return mLifecycle; 219 } 220 221 @Override getInstanceId()222 public InstanceId getInstanceId() { 223 return mInstanceId; 224 } 225 226 /** 227 * Adds or removes a listening client for the tile. If the tile has one or more 228 * listening client it will go into the listening state. 229 */ setListening(Object listener, boolean listening)230 public void setListening(Object listener, boolean listening) { 231 mHandler.obtainMessage(H.SET_LISTENING, listening ? 1 : 0, 0, listener).sendToTarget(); 232 } 233 getStaleTimeout()234 protected long getStaleTimeout() { 235 return DEFAULT_STALE_TIMEOUT; 236 } 237 238 @VisibleForTesting handleStale()239 protected void handleStale() { 240 if (!mListeners.isEmpty()) { 241 // If the tile is already listening (it's been a long time since it refreshed), just 242 // force a refresh. Don't add the staleListener because there's already a listener there 243 refreshState(); 244 } else { 245 setListening(mStaleListener, true); 246 } 247 } 248 getTileSpec()249 public String getTileSpec() { 250 return mTileSpec; 251 } 252 setTileSpec(String tileSpec)253 public void setTileSpec(String tileSpec) { 254 mTileSpec = tileSpec; 255 mState.spec = tileSpec; 256 mTmpState.spec = tileSpec; 257 } 258 getHost()259 public QSHost getHost() { 260 return mHost; 261 } 262 263 /** 264 * Is a startup check whether this device currently supports this tile. 265 * Should not be used to conditionally hide tiles. Only checked on tile 266 * creation or whether should be shown in edit screen. 267 */ isAvailable()268 public boolean isAvailable() { 269 return true; 270 } 271 272 // safe to call from any thread 273 addCallback(Callback callback)274 public void addCallback(Callback callback) { 275 mHandler.obtainMessage(H.ADD_CALLBACK, callback).sendToTarget(); 276 } 277 removeCallback(Callback callback)278 public void removeCallback(Callback callback) { 279 mHandler.obtainMessage(H.REMOVE_CALLBACK, callback).sendToTarget(); 280 } 281 removeCallbacks()282 public void removeCallbacks() { 283 mHandler.sendEmptyMessage(H.REMOVE_CALLBACKS); 284 } 285 286 @Override click(@ullable Expandable expandable)287 public void click(@Nullable Expandable expandable) { 288 mMetricsLogger.write(populate(new LogMaker(ACTION_QS_CLICK).setType(TYPE_ACTION) 289 .addTaggedData(FIELD_STATUS_BAR_STATE, 290 mStatusBarStateController.getState()))); 291 mUiEventLogger.logWithInstanceId(QSEvent.QS_ACTION_CLICK, 0, getMetricsSpec(), 292 getInstanceId()); 293 final int eventId = mClickEventId++; 294 mQSLogger.logTileClick(mTileSpec, mStatusBarStateController.getState(), mState.state, 295 eventId); 296 if (!mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) { 297 mHandler.obtainMessage(H.CLICK, eventId, 0, expandable).sendToTarget(); 298 } 299 } 300 301 @Override secondaryClick(@ullable Expandable expandable)302 public void secondaryClick(@Nullable Expandable expandable) { 303 mMetricsLogger.write(populate(new LogMaker(ACTION_QS_SECONDARY_CLICK).setType(TYPE_ACTION) 304 .addTaggedData(FIELD_STATUS_BAR_STATE, 305 mStatusBarStateController.getState()))); 306 mUiEventLogger.logWithInstanceId(QSEvent.QS_ACTION_SECONDARY_CLICK, 0, getMetricsSpec(), 307 getInstanceId()); 308 final int eventId = mClickEventId++; 309 mQSLogger.logTileSecondaryClick(mTileSpec, mStatusBarStateController.getState(), 310 mState.state, eventId); 311 mHandler.obtainMessage(H.SECONDARY_CLICK, eventId, 0, expandable).sendToTarget(); 312 } 313 314 @Override longClick(@ullable Expandable expandable)315 public void longClick(@Nullable Expandable expandable) { 316 mMetricsLogger.write(populate(new LogMaker(ACTION_QS_LONG_PRESS).setType(TYPE_ACTION) 317 .addTaggedData(FIELD_STATUS_BAR_STATE, 318 mStatusBarStateController.getState()))); 319 mUiEventLogger.logWithInstanceId(QSEvent.QS_ACTION_LONG_PRESS, 0, getMetricsSpec(), 320 getInstanceId()); 321 final int eventId = mClickEventId++; 322 mQSLogger.logTileLongClick(mTileSpec, mStatusBarStateController.getState(), mState.state, 323 eventId); 324 if (!mFalsingManager.isFalseLongTap(FalsingManager.LOW_PENALTY)) { 325 mHandler.obtainMessage(H.LONG_CLICK, eventId, 0, expandable).sendToTarget(); 326 } 327 } 328 populate(LogMaker logMaker)329 public LogMaker populate(LogMaker logMaker) { 330 if (mState instanceof BooleanState) { 331 logMaker.addTaggedData(FIELD_QS_VALUE, ((BooleanState) mState).value ? 1 : 0); 332 } 333 return logMaker.setSubtype(getMetricsCategory()) 334 .addTaggedData(FIELD_IS_FULL_QS, mIsFullQs) 335 .addTaggedData(FIELD_QS_POSITION, mHost.indexOf(mTileSpec)); 336 } 337 refreshState()338 public void refreshState() { 339 refreshState(null); 340 } 341 342 @Override isListening()343 public final boolean isListening() { 344 return getLifecycle().getCurrentState().isAtLeast(RESUMED); 345 } 346 refreshState(@ullable Object arg)347 protected final void refreshState(@Nullable Object arg) { 348 mHandler.obtainMessage(H.REFRESH_STATE, arg).sendToTarget(); 349 } 350 userSwitch(int newUserId)351 public void userSwitch(int newUserId) { 352 mHandler.obtainMessage(H.USER_SWITCH, newUserId, 0).sendToTarget(); 353 } 354 destroy()355 public void destroy() { 356 mHandler.sendEmptyMessage(H.DESTROY); 357 } 358 359 /** 360 * Schedules initialization of the tile. 361 * 362 * Should be called upon creation of the tile, before performing other operations 363 */ initialize()364 public void initialize() { 365 mHandler.sendEmptyMessage(H.INITIALIZE); 366 } 367 getState()368 public TState getState() { 369 return mState; 370 } 371 setDetailListening(boolean listening)372 public void setDetailListening(boolean listening) { 373 // optional 374 } 375 376 // call only on tile worker looper 377 handleAddCallback(Callback callback)378 private void handleAddCallback(Callback callback) { 379 mCallbacks.add(callback); 380 callback.onStateChanged(mState); 381 } 382 handleRemoveCallback(Callback callback)383 private void handleRemoveCallback(Callback callback) { 384 mCallbacks.remove(callback); 385 } 386 handleRemoveCallbacks()387 private void handleRemoveCallbacks() { 388 mCallbacks.clear(); 389 } 390 391 /** 392 * Posts a stale message to the background thread. 393 */ postStale()394 public void postStale() { 395 mHandler.sendEmptyMessage(H.STALE); 396 } 397 398 /** 399 * Handles secondary click on the tile. 400 * 401 * Defaults to {@link QSTileImpl#handleClick} 402 * 403 * @param expandable {@link Expandable} that was clicked. 404 */ handleSecondaryClick(@ullable Expandable expandable)405 protected void handleSecondaryClick(@Nullable Expandable expandable) { 406 // Default to normal click. 407 handleClick(expandable); 408 } 409 410 /** 411 * Handles long click on the tile by launching the {@link Intent} defined in 412 * {@link QSTileImpl#getLongClickIntent}. 413 * 414 * @param expandable {@link Expandable} from which the opening window will be animated. 415 */ handleLongClick(@ullable Expandable expandable)416 protected void handleLongClick(@Nullable Expandable expandable) { 417 ActivityTransitionAnimator.Controller animationController = 418 expandable != null ? expandable.activityTransitionController( 419 InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_QS_TILE) : null; 420 mActivityStarter.postStartActivityDismissingKeyguard(getLongClickIntent(), 0, 421 animationController); 422 } 423 424 /** 425 * Returns an intent to be launched when the tile is long pressed. 426 * 427 * @return the intent to launch 428 */ 429 @Nullable getLongClickIntent()430 public abstract Intent getLongClickIntent(); 431 handleRefreshState(@ullable Object arg)432 protected final void handleRefreshState(@Nullable Object arg) { 433 handleUpdateState(mTmpState, arg); 434 boolean changed = mTmpState.copyTo(mState); 435 if (mReadyState == READY_STATE_READYING) { 436 mReadyState = READY_STATE_READY; 437 changed = true; 438 } 439 if (changed) { 440 mQSLogger.logTileUpdated(mTileSpec, mState); 441 handleStateChanged(); 442 } 443 mHandler.removeMessages(H.STALE); 444 mHandler.sendEmptyMessageDelayed(H.STALE, getStaleTimeout()); 445 setListening(mStaleListener, false); 446 } 447 handleStateChanged()448 private void handleStateChanged() { 449 if (!mCallbacks.isEmpty()) { 450 for (int i = 0; i < mCallbacks.size(); i++) { 451 mCallbacks.valueAt(i).onStateChanged(mState); 452 } 453 } 454 } 455 handleUserSwitch(int newUserId)456 protected void handleUserSwitch(int newUserId) { 457 handleRefreshState(null); 458 } 459 handleSetListeningInternal(Object listener, boolean listening)460 private void handleSetListeningInternal(Object listener, boolean listening) { 461 // This should be used to go from resumed to paused. Listening for ON_RESUME and ON_PAUSE 462 // in this lifecycle will determine the listening window. 463 if (listening) { 464 if (mListeners.add(listener) && mListeners.size() == 1) { 465 if (DEBUG) Log.d(TAG, "handleSetListening true"); 466 handleSetListening(listening); 467 mUiHandler.post(() -> { 468 // This tile has been destroyed, the state should not change anymore and we 469 // should not refresh it anymore. 470 if (mLifecycle.getCurrentState().equals(DESTROYED)) return; 471 mLifecycle.setCurrentState(RESUMED); 472 if (mReadyState == READY_STATE_NOT_READY) { 473 mReadyState = READY_STATE_READYING; 474 } 475 refreshState(); // Ensure we get at least one refresh after listening. 476 }); 477 } 478 } else { 479 if (mListeners.remove(listener) && mListeners.size() == 0) { 480 if (DEBUG) Log.d(TAG, "handleSetListening false"); 481 handleSetListening(listening); 482 mUiHandler.post(() -> { 483 // This tile has been destroyed, the state should not change anymore. 484 if (mLifecycle.getCurrentState().equals(DESTROYED)) return; 485 mLifecycle.setCurrentState(STARTED); 486 }); 487 } 488 } 489 updateIsFullQs(); 490 } 491 updateIsFullQs()492 private void updateIsFullQs() { 493 for (Object listener : mListeners) { 494 if (SideLabelTileLayout.class.equals(listener.getClass())) { 495 mIsFullQs = 1; 496 return; 497 } 498 } 499 mIsFullQs = 0; 500 } 501 502 @CallSuper handleSetListening(boolean listening)503 protected void handleSetListening(boolean listening) { 504 if (mTileSpec != null) { 505 mQSLogger.logTileChangeListening(mTileSpec, listening); 506 } 507 } 508 handleDestroy()509 protected void handleDestroy() { 510 mQSLogger.logTileDestroyed(mTileSpec, "Handle destroy"); 511 if (mListeners.size() != 0) { 512 handleSetListening(false); 513 mListeners.clear(); 514 } 515 mCallbacks.clear(); 516 mHandler.removeCallbacksAndMessages(null); 517 // This will force it to be removed from all controllers that may have it registered. 518 mUiHandler.post(() -> { 519 mLifecycle.setCurrentState(DESTROYED); 520 }); 521 } 522 checkIfRestrictionEnforcedByAdminOnly(State state, String userRestriction)523 protected void checkIfRestrictionEnforcedByAdminOnly(State state, String userRestriction) { 524 EnforcedAdmin admin = RestrictedLockUtilsInternal.checkIfRestrictionEnforced(mContext, 525 userRestriction, mHost.getUserId()); 526 if (admin != null && !RestrictedLockUtilsInternal.hasBaseUserRestriction(mContext, 527 userRestriction, mHost.getUserId())) { 528 state.disabledByPolicy = true; 529 mEnforcedAdmin = admin; 530 } else { 531 state.disabledByPolicy = false; 532 mEnforcedAdmin = null; 533 } 534 } 535 536 @Override getMetricsSpec()537 public String getMetricsSpec() { 538 return mTileSpec; 539 } 540 541 /** 542 * Provides a default label for the tile. 543 * @return default label for the tile. 544 */ getTileLabel()545 public abstract CharSequence getTileLabel(); 546 547 /** 548 * @return {@code true} if the tile has refreshed state at least once after having set its 549 * lifecycle to {@link Lifecycle.State#RESUMED}. 550 */ 551 @Override isTileReady()552 public boolean isTileReady() { 553 return mReadyState == READY_STATE_READY; 554 } 555 556 protected final class H extends Handler { 557 private static final int ADD_CALLBACK = 1; 558 private static final int CLICK = 2; 559 private static final int SECONDARY_CLICK = 3; 560 private static final int LONG_CLICK = 4; 561 private static final int REFRESH_STATE = 5; 562 private static final int USER_SWITCH = 6; 563 private static final int DESTROY = 7; 564 private static final int REMOVE_CALLBACKS = 8; 565 private static final int REMOVE_CALLBACK = 9; 566 private static final int SET_LISTENING = 10; 567 @VisibleForTesting 568 protected static final int STALE = 11; 569 private static final int INITIALIZE = 12; 570 571 @VisibleForTesting H(Looper looper)572 protected H(Looper looper) { 573 super(looper); 574 } 575 576 @Override handleMessage(Message msg)577 public void handleMessage(Message msg) { 578 String name = null; 579 try { 580 if (msg.what == ADD_CALLBACK) { 581 name = "handleAddCallback"; 582 handleAddCallback((QSTile.Callback) msg.obj); 583 } else if (msg.what == REMOVE_CALLBACKS) { 584 name = "handleRemoveCallbacks"; 585 handleRemoveCallbacks(); 586 } else if (msg.what == REMOVE_CALLBACK) { 587 name = "handleRemoveCallback"; 588 handleRemoveCallback((QSTile.Callback) msg.obj); 589 } else if (msg.what == CLICK) { 590 name = "handleClick"; 591 if (mState.disabledByPolicy) { 592 Intent intent = RestrictedLockUtils.getShowAdminSupportDetailsIntent( 593 mEnforcedAdmin); 594 mActivityStarter.postStartActivityDismissingKeyguard(intent, 0); 595 } else { 596 mQSLogger.logHandleClick(mTileSpec, msg.arg1); 597 handleClick((Expandable) msg.obj); 598 } 599 } else if (msg.what == SECONDARY_CLICK) { 600 name = "handleSecondaryClick"; 601 mQSLogger.logHandleSecondaryClick(mTileSpec, msg.arg1); 602 handleSecondaryClick((Expandable) msg.obj); 603 } else if (msg.what == LONG_CLICK) { 604 name = "handleLongClick"; 605 mQSLogger.logHandleLongClick(mTileSpec, msg.arg1); 606 handleLongClick((Expandable) msg.obj); 607 } else if (msg.what == REFRESH_STATE) { 608 name = "handleRefreshState"; 609 handleRefreshState(msg.obj); 610 } else if (msg.what == USER_SWITCH) { 611 name = "handleUserSwitch"; 612 handleUserSwitch(msg.arg1); 613 } else if (msg.what == DESTROY) { 614 name = "handleDestroy"; 615 handleDestroy(); 616 } else if (msg.what == SET_LISTENING) { 617 name = "handleSetListeningInternal"; 618 handleSetListeningInternal(msg.obj, msg.arg1 != 0); 619 } else if (msg.what == STALE) { 620 name = "handleStale"; 621 handleStale(); 622 } else if (msg.what == INITIALIZE) { 623 name = "initialize"; 624 handleInitialize(); 625 } else { 626 throw new IllegalArgumentException("Unknown msg: " + msg.what); 627 } 628 } catch (Throwable t) { 629 final String error = "Error in " + name; 630 Log.w(TAG, error, t); 631 } 632 } 633 } 634 635 public static class DrawableIcon extends Icon { 636 637 protected final Drawable mDrawable; 638 protected final Drawable mInvisibleDrawable; 639 private static final String TAG = "QSTileImpl"; 640 DrawableIcon(Drawable drawable)641 public DrawableIcon(Drawable drawable) { 642 mDrawable = drawable; 643 Drawable.ConstantState nullableConstantState = drawable.getConstantState(); 644 if (nullableConstantState == null) { 645 if (!(drawable instanceof SignalDrawable)) { 646 Log.w(TAG, "DrawableIcon: drawable has null ConstantState" 647 + " and is not a SignalDrawable"); 648 } 649 mInvisibleDrawable = drawable; 650 } else { 651 mInvisibleDrawable = nullableConstantState.newDrawable(); 652 } 653 } 654 655 @Override getDrawable(Context context)656 public Drawable getDrawable(Context context) { 657 return mDrawable; 658 } 659 660 @Override getInvisibleDrawable(Context context)661 public Drawable getInvisibleDrawable(Context context) { 662 return mInvisibleDrawable; 663 } 664 665 @Override 666 @NonNull toString()667 public String toString() { 668 return "DrawableIcon"; 669 } 670 } 671 672 public static class DrawableIconWithRes extends DrawableIcon { 673 private final int mId; 674 DrawableIconWithRes(Drawable drawable, int id)675 public DrawableIconWithRes(Drawable drawable, int id) { 676 super(drawable); 677 mId = id; 678 } 679 getResourceId()680 public int getResourceId() { 681 return mId; 682 } 683 684 @Override equals(Object o)685 public boolean equals(Object o) { 686 return o instanceof DrawableIconWithRes && ((DrawableIconWithRes) o).mId == mId; 687 } 688 689 @Override 690 @NonNull toString()691 public String toString() { 692 return String.format("DrawableIconWithRes[resId=0x%08x]", mId); 693 } 694 } 695 696 public static class ResourceIcon extends Icon { 697 private static final SparseArray<Icon> ICONS = new SparseArray<Icon>(); 698 699 protected final int mResId; 700 ResourceIcon(int resId)701 private ResourceIcon(int resId) { 702 mResId = resId; 703 } 704 get(int resId)705 public static synchronized Icon get(int resId) { 706 Icon icon = ICONS.get(resId); 707 if (icon == null) { 708 icon = new ResourceIcon(resId); 709 ICONS.put(resId, icon); 710 } 711 return icon; 712 } 713 714 @Override getDrawable(Context context)715 public Drawable getDrawable(Context context) { 716 return context.getDrawable(mResId); 717 } 718 719 @Override getInvisibleDrawable(Context context)720 public Drawable getInvisibleDrawable(Context context) { 721 return context.getDrawable(mResId); 722 } 723 getResId()724 public int getResId() { 725 return mResId; 726 } 727 728 @Override equals(Object o)729 public boolean equals(Object o) { 730 return o instanceof ResourceIcon && ((ResourceIcon) o).mResId == mResId; 731 } 732 733 @Override 734 @NonNull toString()735 public String toString() { 736 return String.format("ResourceIcon[resId=0x%08x]", mResId); 737 } 738 } 739 740 protected static class AnimationIcon extends ResourceIcon { 741 private final int mAnimatedResId; 742 AnimationIcon(int resId, int staticResId)743 public AnimationIcon(int resId, int staticResId) { 744 super(staticResId); 745 mAnimatedResId = resId; 746 } 747 748 @Override getDrawable(Context context)749 public Drawable getDrawable(Context context) { 750 // workaround: get a clean state for every new AVD 751 return context.getDrawable(mAnimatedResId).getConstantState().newDrawable(); 752 } 753 754 @Override 755 @NonNull toString()756 public String toString() { 757 return String.format("AnimationIcon[resId=0x%08x]", mResId); 758 } 759 } 760 761 /** 762 * Dumps the state of this tile along with its name. 763 * 764 * This may be used for CTS testing of tiles. 765 */ 766 @Override dump(PrintWriter pw, String[] args)767 public void dump(PrintWriter pw, String[] args) { 768 pw.println(this.getClass().getSimpleName() + ":"); 769 pw.print(" "); pw.println(getState().toString()); 770 } 771 } 772