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 import android.view.View; 46 47 import androidx.annotation.Nullable; 48 import androidx.lifecycle.Lifecycle; 49 import androidx.lifecycle.LifecycleOwner; 50 import androidx.lifecycle.LifecycleRegistry; 51 52 import com.android.internal.annotations.VisibleForTesting; 53 import com.android.internal.jank.InteractionJankMonitor; 54 import com.android.internal.logging.InstanceId; 55 import com.android.internal.logging.MetricsLogger; 56 import com.android.internal.logging.UiEventLogger; 57 import com.android.settingslib.RestrictedLockUtils; 58 import com.android.settingslib.RestrictedLockUtilsInternal; 59 import com.android.systemui.Dumpable; 60 import com.android.systemui.animation.ActivityLaunchAnimator; 61 import com.android.systemui.plugins.ActivityStarter; 62 import com.android.systemui.plugins.FalsingManager; 63 import com.android.systemui.plugins.qs.QSIconView; 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.SideLabelTileLayout; 70 import com.android.systemui.qs.logging.QSLogger; 71 72 import java.io.PrintWriter; 73 import java.util.ArrayList; 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 static 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 ArrayList<Callback> mCallbacks = new ArrayList<>(); 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 view The view that was clicked. 142 */ handleClick(@ullable View view)143 protected abstract void handleClick(@Nullable View view); 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, Looper backgroundLooper, Handler mainHandler, FalsingManager falsingManager, MetricsLogger metricsLogger, StatusBarStateController statusBarStateController, ActivityStarter activityStarter, QSLogger qsLogger )180 protected QSTileImpl( 181 QSHost host, 182 Looper backgroundLooper, 183 Handler mainHandler, 184 FalsingManager falsingManager, 185 MetricsLogger metricsLogger, 186 StatusBarStateController statusBarStateController, 187 ActivityStarter activityStarter, 188 QSLogger qsLogger 189 ) { 190 mHost = host; 191 mContext = host.getContext(); 192 mInstanceId = host.getNewInstanceId(); 193 mUiEventLogger = host.getUiEventLogger(); 194 195 mUiHandler = mainHandler; 196 mHandler = new H(backgroundLooper); 197 mFalsingManager = falsingManager; 198 mQSLogger = qsLogger; 199 mMetricsLogger = metricsLogger; 200 mStatusBarStateController = statusBarStateController; 201 mActivityStarter = activityStarter; 202 203 resetStates(); 204 mUiHandler.post(() -> mLifecycle.setCurrentState(CREATED)); 205 } 206 resetStates()207 protected final void resetStates() { 208 mState = newTileState(); 209 mTmpState = newTileState(); 210 mState.spec = mTileSpec; 211 mTmpState.spec = mTileSpec; 212 } 213 214 @NonNull 215 @Override getLifecycle()216 public Lifecycle getLifecycle() { 217 return mLifecycle; 218 } 219 220 @Override getInstanceId()221 public InstanceId getInstanceId() { 222 return mInstanceId; 223 } 224 225 /** 226 * Adds or removes a listening client for the tile. If the tile has one or more 227 * listening client it will go into the listening state. 228 */ setListening(Object listener, boolean listening)229 public void setListening(Object listener, boolean listening) { 230 mHandler.obtainMessage(H.SET_LISTENING, listening ? 1 : 0, 0, listener).sendToTarget(); 231 } 232 getStaleTimeout()233 protected long getStaleTimeout() { 234 return DEFAULT_STALE_TIMEOUT; 235 } 236 237 @VisibleForTesting handleStale()238 protected void handleStale() { 239 if (!mListeners.isEmpty()) { 240 // If the tile is already listening (it's been a long time since it refreshed), just 241 // force a refresh. Don't add the staleListener because there's already a listener there 242 refreshState(); 243 } else { 244 setListening(mStaleListener, true); 245 } 246 } 247 getTileSpec()248 public String getTileSpec() { 249 return mTileSpec; 250 } 251 setTileSpec(String tileSpec)252 public void setTileSpec(String tileSpec) { 253 mTileSpec = tileSpec; 254 mState.spec = tileSpec; 255 mTmpState.spec = tileSpec; 256 } 257 getHost()258 public QSHost getHost() { 259 return mHost; 260 } 261 262 /** 263 * Return the {@link QSIconView} to be used by this tile's view. 264 * 265 * @param context view context for the view 266 * @return icon view for this tile 267 */ createTileView(Context context)268 public QSIconView createTileView(Context context) { 269 return new QSIconViewImpl(context); 270 } 271 272 /** 273 * Is a startup check whether this device currently supports this tile. 274 * Should not be used to conditionally hide tiles. Only checked on tile 275 * creation or whether should be shown in edit screen. 276 */ isAvailable()277 public boolean isAvailable() { 278 return true; 279 } 280 281 // safe to call from any thread 282 addCallback(Callback callback)283 public void addCallback(Callback callback) { 284 mHandler.obtainMessage(H.ADD_CALLBACK, callback).sendToTarget(); 285 } 286 removeCallback(Callback callback)287 public void removeCallback(Callback callback) { 288 mHandler.obtainMessage(H.REMOVE_CALLBACK, callback).sendToTarget(); 289 } 290 removeCallbacks()291 public void removeCallbacks() { 292 mHandler.sendEmptyMessage(H.REMOVE_CALLBACKS); 293 } 294 click(@ullable View view)295 public void click(@Nullable View view) { 296 mMetricsLogger.write(populate(new LogMaker(ACTION_QS_CLICK).setType(TYPE_ACTION) 297 .addTaggedData(FIELD_STATUS_BAR_STATE, 298 mStatusBarStateController.getState()))); 299 mUiEventLogger.logWithInstanceId(QSEvent.QS_ACTION_CLICK, 0, getMetricsSpec(), 300 getInstanceId()); 301 final int eventId = mClickEventId++; 302 mQSLogger.logTileClick(mTileSpec, mStatusBarStateController.getState(), mState.state, 303 eventId); 304 if (!mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) { 305 mHandler.obtainMessage(H.CLICK, eventId, 0, view).sendToTarget(); 306 } 307 } 308 secondaryClick(@ullable View view)309 public void secondaryClick(@Nullable View view) { 310 mMetricsLogger.write(populate(new LogMaker(ACTION_QS_SECONDARY_CLICK).setType(TYPE_ACTION) 311 .addTaggedData(FIELD_STATUS_BAR_STATE, 312 mStatusBarStateController.getState()))); 313 mUiEventLogger.logWithInstanceId(QSEvent.QS_ACTION_SECONDARY_CLICK, 0, getMetricsSpec(), 314 getInstanceId()); 315 final int eventId = mClickEventId++; 316 mQSLogger.logTileSecondaryClick(mTileSpec, mStatusBarStateController.getState(), 317 mState.state, eventId); 318 mHandler.obtainMessage(H.SECONDARY_CLICK, eventId, 0, view).sendToTarget(); 319 } 320 321 @Override longClick(@ullable View view)322 public void longClick(@Nullable View view) { 323 mMetricsLogger.write(populate(new LogMaker(ACTION_QS_LONG_PRESS).setType(TYPE_ACTION) 324 .addTaggedData(FIELD_STATUS_BAR_STATE, 325 mStatusBarStateController.getState()))); 326 mUiEventLogger.logWithInstanceId(QSEvent.QS_ACTION_LONG_PRESS, 0, getMetricsSpec(), 327 getInstanceId()); 328 final int eventId = mClickEventId++; 329 mQSLogger.logTileLongClick(mTileSpec, mStatusBarStateController.getState(), mState.state, 330 eventId); 331 mHandler.obtainMessage(H.LONG_CLICK, eventId, 0, view).sendToTarget(); 332 } 333 populate(LogMaker logMaker)334 public LogMaker populate(LogMaker logMaker) { 335 if (mState instanceof BooleanState) { 336 logMaker.addTaggedData(FIELD_QS_VALUE, ((BooleanState) mState).value ? 1 : 0); 337 } 338 return logMaker.setSubtype(getMetricsCategory()) 339 .addTaggedData(FIELD_IS_FULL_QS, mIsFullQs) 340 .addTaggedData(FIELD_QS_POSITION, mHost.indexOf(mTileSpec)); 341 } 342 refreshState()343 public void refreshState() { 344 refreshState(null); 345 } 346 347 @Override isListening()348 public final boolean isListening() { 349 return getLifecycle().getCurrentState().isAtLeast(RESUMED); 350 } 351 refreshState(@ullable Object arg)352 protected final void refreshState(@Nullable Object arg) { 353 mHandler.obtainMessage(H.REFRESH_STATE, arg).sendToTarget(); 354 } 355 userSwitch(int newUserId)356 public void userSwitch(int newUserId) { 357 mHandler.obtainMessage(H.USER_SWITCH, newUserId, 0).sendToTarget(); 358 } 359 destroy()360 public void destroy() { 361 mHandler.sendEmptyMessage(H.DESTROY); 362 } 363 364 /** 365 * Schedules initialization of the tile. 366 * 367 * Should be called upon creation of the tile, before performing other operations 368 */ initialize()369 public void initialize() { 370 mHandler.sendEmptyMessage(H.INITIALIZE); 371 } 372 getState()373 public TState getState() { 374 return mState; 375 } 376 setDetailListening(boolean listening)377 public void setDetailListening(boolean listening) { 378 // optional 379 } 380 381 // call only on tile worker looper 382 handleAddCallback(Callback callback)383 private void handleAddCallback(Callback callback) { 384 mCallbacks.add(callback); 385 callback.onStateChanged(mState); 386 } 387 handleRemoveCallback(Callback callback)388 private void handleRemoveCallback(Callback callback) { 389 mCallbacks.remove(callback); 390 } 391 handleRemoveCallbacks()392 private void handleRemoveCallbacks() { 393 mCallbacks.clear(); 394 } 395 396 /** 397 * Posts a stale message to the background thread. 398 */ postStale()399 public void postStale() { 400 mHandler.sendEmptyMessage(H.STALE); 401 } 402 403 /** 404 * Handles secondary click on the tile. 405 * 406 * Defaults to {@link QSTileImpl#handleClick} 407 * 408 * @param view The view that was clicked. 409 */ handleSecondaryClick(@ullable View view)410 protected void handleSecondaryClick(@Nullable View view) { 411 // Default to normal click. 412 handleClick(view); 413 } 414 415 /** 416 * Handles long click on the tile by launching the {@link Intent} defined in 417 * {@link QSTileImpl#getLongClickIntent}. 418 * 419 * @param view The view from which the opening window will be animated. 420 */ handleLongClick(@ullable View view)421 protected void handleLongClick(@Nullable View view) { 422 ActivityLaunchAnimator.Controller animationController = 423 view != null ? ActivityLaunchAnimator.Controller.fromView(view, 424 InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_QS_TILE) : null; 425 mActivityStarter.postStartActivityDismissingKeyguard(getLongClickIntent(), 0, 426 animationController); 427 } 428 429 /** 430 * Returns an intent to be launched when the tile is long pressed. 431 * 432 * @return the intent to launch 433 */ 434 @Nullable getLongClickIntent()435 public abstract Intent getLongClickIntent(); 436 handleRefreshState(@ullable Object arg)437 protected final void handleRefreshState(@Nullable Object arg) { 438 handleUpdateState(mTmpState, arg); 439 boolean changed = mTmpState.copyTo(mState); 440 if (mReadyState == READY_STATE_READYING) { 441 mReadyState = READY_STATE_READY; 442 changed = true; 443 } 444 if (changed) { 445 mQSLogger.logTileUpdated(mTileSpec, mState); 446 handleStateChanged(); 447 } 448 mHandler.removeMessages(H.STALE); 449 mHandler.sendEmptyMessageDelayed(H.STALE, getStaleTimeout()); 450 setListening(mStaleListener, false); 451 } 452 handleStateChanged()453 private void handleStateChanged() { 454 if (mCallbacks.size() != 0) { 455 for (int i = 0; i < mCallbacks.size(); i++) { 456 mCallbacks.get(i).onStateChanged(mState); 457 } 458 } 459 } 460 handleUserSwitch(int newUserId)461 protected void handleUserSwitch(int newUserId) { 462 handleRefreshState(null); 463 } 464 handleSetListeningInternal(Object listener, boolean listening)465 private void handleSetListeningInternal(Object listener, boolean listening) { 466 // This should be used to go from resumed to paused. Listening for ON_RESUME and ON_PAUSE 467 // in this lifecycle will determine the listening window. 468 if (listening) { 469 if (mListeners.add(listener) && mListeners.size() == 1) { 470 if (DEBUG) Log.d(TAG, "handleSetListening true"); 471 handleSetListening(listening); 472 mUiHandler.post(() -> { 473 // This tile has been destroyed, the state should not change anymore and we 474 // should not refresh it anymore. 475 if (mLifecycle.getCurrentState().equals(DESTROYED)) return; 476 mLifecycle.setCurrentState(RESUMED); 477 if (mReadyState == READY_STATE_NOT_READY) { 478 mReadyState = READY_STATE_READYING; 479 } 480 refreshState(); // Ensure we get at least one refresh after listening. 481 }); 482 } 483 } else { 484 if (mListeners.remove(listener) && mListeners.size() == 0) { 485 if (DEBUG) Log.d(TAG, "handleSetListening false"); 486 handleSetListening(listening); 487 mUiHandler.post(() -> { 488 // This tile has been destroyed, the state should not change anymore. 489 if (mLifecycle.getCurrentState().equals(DESTROYED)) return; 490 mLifecycle.setCurrentState(STARTED); 491 }); 492 } 493 } 494 updateIsFullQs(); 495 } 496 updateIsFullQs()497 private void updateIsFullQs() { 498 for (Object listener : mListeners) { 499 if (SideLabelTileLayout.class.equals(listener.getClass())) { 500 mIsFullQs = 1; 501 return; 502 } 503 } 504 mIsFullQs = 0; 505 } 506 507 @CallSuper handleSetListening(boolean listening)508 protected void handleSetListening(boolean listening) { 509 if (mTileSpec != null) { 510 mQSLogger.logTileChangeListening(mTileSpec, listening); 511 } 512 } 513 handleDestroy()514 protected void handleDestroy() { 515 mQSLogger.logTileDestroyed(mTileSpec, "Handle destroy"); 516 if (mListeners.size() != 0) { 517 handleSetListening(false); 518 mListeners.clear(); 519 } 520 mCallbacks.clear(); 521 mHandler.removeCallbacksAndMessages(null); 522 // This will force it to be removed from all controllers that may have it registered. 523 mUiHandler.post(() -> { 524 mLifecycle.setCurrentState(DESTROYED); 525 }); 526 } 527 checkIfRestrictionEnforcedByAdminOnly(State state, String userRestriction)528 protected void checkIfRestrictionEnforcedByAdminOnly(State state, String userRestriction) { 529 EnforcedAdmin admin = RestrictedLockUtilsInternal.checkIfRestrictionEnforced(mContext, 530 userRestriction, mHost.getUserId()); 531 if (admin != null && !RestrictedLockUtilsInternal.hasBaseUserRestriction(mContext, 532 userRestriction, mHost.getUserId())) { 533 state.disabledByPolicy = true; 534 mEnforcedAdmin = admin; 535 } else { 536 state.disabledByPolicy = false; 537 mEnforcedAdmin = null; 538 } 539 } 540 541 @Override getMetricsSpec()542 public String getMetricsSpec() { 543 return mTileSpec; 544 } 545 546 /** 547 * Provides a default label for the tile. 548 * @return default label for the tile. 549 */ getTileLabel()550 public abstract CharSequence getTileLabel(); 551 552 /** 553 * @return {@code true} if the tile has refreshed state at least once after having set its 554 * lifecycle to {@link Lifecycle.State#RESUMED}. 555 */ 556 @Override isTileReady()557 public boolean isTileReady() { 558 return mReadyState == READY_STATE_READY; 559 } 560 561 protected final class H extends Handler { 562 private static final int ADD_CALLBACK = 1; 563 private static final int CLICK = 2; 564 private static final int SECONDARY_CLICK = 3; 565 private static final int LONG_CLICK = 4; 566 private static final int REFRESH_STATE = 5; 567 private static final int USER_SWITCH = 6; 568 private static final int DESTROY = 7; 569 private static final int REMOVE_CALLBACKS = 8; 570 private static final int REMOVE_CALLBACK = 9; 571 private static final int SET_LISTENING = 10; 572 @VisibleForTesting 573 protected static final int STALE = 11; 574 private static final int INITIALIZE = 12; 575 576 @VisibleForTesting H(Looper looper)577 protected H(Looper looper) { 578 super(looper); 579 } 580 581 @Override handleMessage(Message msg)582 public void handleMessage(Message msg) { 583 String name = null; 584 try { 585 if (msg.what == ADD_CALLBACK) { 586 name = "handleAddCallback"; 587 handleAddCallback((QSTile.Callback) msg.obj); 588 } else if (msg.what == REMOVE_CALLBACKS) { 589 name = "handleRemoveCallbacks"; 590 handleRemoveCallbacks(); 591 } else if (msg.what == REMOVE_CALLBACK) { 592 name = "handleRemoveCallback"; 593 handleRemoveCallback((QSTile.Callback) msg.obj); 594 } else if (msg.what == CLICK) { 595 name = "handleClick"; 596 if (mState.disabledByPolicy) { 597 Intent intent = RestrictedLockUtils.getShowAdminSupportDetailsIntent( 598 mContext, mEnforcedAdmin); 599 mActivityStarter.postStartActivityDismissingKeyguard(intent, 0); 600 } else { 601 mQSLogger.logHandleClick(mTileSpec, msg.arg1); 602 handleClick((View) msg.obj); 603 } 604 } else if (msg.what == SECONDARY_CLICK) { 605 name = "handleSecondaryClick"; 606 mQSLogger.logHandleSecondaryClick(mTileSpec, msg.arg1); 607 handleSecondaryClick((View) msg.obj); 608 } else if (msg.what == LONG_CLICK) { 609 name = "handleLongClick"; 610 mQSLogger.logHandleLongClick(mTileSpec, msg.arg1); 611 handleLongClick((View) msg.obj); 612 } else if (msg.what == REFRESH_STATE) { 613 name = "handleRefreshState"; 614 handleRefreshState(msg.obj); 615 } else if (msg.what == USER_SWITCH) { 616 name = "handleUserSwitch"; 617 handleUserSwitch(msg.arg1); 618 } else if (msg.what == DESTROY) { 619 name = "handleDestroy"; 620 handleDestroy(); 621 } else if (msg.what == SET_LISTENING) { 622 name = "handleSetListeningInternal"; 623 handleSetListeningInternal(msg.obj, msg.arg1 != 0); 624 } else if (msg.what == STALE) { 625 name = "handleStale"; 626 handleStale(); 627 } else if (msg.what == INITIALIZE) { 628 name = "initialize"; 629 handleInitialize(); 630 } else { 631 throw new IllegalArgumentException("Unknown msg: " + msg.what); 632 } 633 } catch (Throwable t) { 634 final String error = "Error in " + name; 635 Log.w(TAG, error, t); 636 mHost.warn(error, t); 637 } 638 } 639 } 640 641 public static class DrawableIcon extends Icon { 642 protected final Drawable mDrawable; 643 protected final Drawable mInvisibleDrawable; 644 DrawableIcon(Drawable drawable)645 public DrawableIcon(Drawable drawable) { 646 mDrawable = drawable; 647 mInvisibleDrawable = drawable.getConstantState().newDrawable(); 648 } 649 650 @Override getDrawable(Context context)651 public Drawable getDrawable(Context context) { 652 return mDrawable; 653 } 654 655 @Override getInvisibleDrawable(Context context)656 public Drawable getInvisibleDrawable(Context context) { 657 return mInvisibleDrawable; 658 } 659 660 @Override 661 @NonNull toString()662 public String toString() { 663 return "DrawableIcon"; 664 } 665 } 666 667 public static class DrawableIconWithRes extends DrawableIcon { 668 private final int mId; 669 DrawableIconWithRes(Drawable drawable, int id)670 public DrawableIconWithRes(Drawable drawable, int id) { 671 super(drawable); 672 mId = id; 673 } 674 675 @Override equals(Object o)676 public boolean equals(Object o) { 677 return o instanceof DrawableIconWithRes && ((DrawableIconWithRes) o).mId == mId; 678 } 679 680 @Override 681 @NonNull toString()682 public String toString() { 683 return String.format("DrawableIconWithRes[resId=0x%08x]", mId); 684 } 685 } 686 687 public static class ResourceIcon extends Icon { 688 private static final SparseArray<Icon> ICONS = new SparseArray<Icon>(); 689 690 protected final int mResId; 691 ResourceIcon(int resId)692 private ResourceIcon(int resId) { 693 mResId = resId; 694 } 695 get(int resId)696 public static synchronized Icon get(int resId) { 697 Icon icon = ICONS.get(resId); 698 if (icon == null) { 699 icon = new ResourceIcon(resId); 700 ICONS.put(resId, icon); 701 } 702 return icon; 703 } 704 705 @Override getDrawable(Context context)706 public Drawable getDrawable(Context context) { 707 return context.getDrawable(mResId); 708 } 709 710 @Override getInvisibleDrawable(Context context)711 public Drawable getInvisibleDrawable(Context context) { 712 return context.getDrawable(mResId); 713 } 714 getResId()715 public int getResId() { 716 return mResId; 717 } 718 719 @Override equals(Object o)720 public boolean equals(Object o) { 721 return o instanceof ResourceIcon && ((ResourceIcon) o).mResId == mResId; 722 } 723 724 @Override 725 @NonNull toString()726 public String toString() { 727 return String.format("ResourceIcon[resId=0x%08x]", mResId); 728 } 729 } 730 731 protected static class AnimationIcon extends ResourceIcon { 732 private final int mAnimatedResId; 733 AnimationIcon(int resId, int staticResId)734 public AnimationIcon(int resId, int staticResId) { 735 super(staticResId); 736 mAnimatedResId = resId; 737 } 738 739 @Override getDrawable(Context context)740 public Drawable getDrawable(Context context) { 741 // workaround: get a clean state for every new AVD 742 return context.getDrawable(mAnimatedResId).getConstantState().newDrawable(); 743 } 744 745 @Override 746 @NonNull toString()747 public String toString() { 748 return String.format("AnimationIcon[resId=0x%08x]", mResId); 749 } 750 } 751 752 /** 753 * Dumps the state of this tile along with its name. 754 * 755 * This may be used for CTS testing of tiles. 756 */ 757 @Override dump(PrintWriter pw, String[] args)758 public void dump(PrintWriter pw, String[] args) { 759 pw.println(this.getClass().getSimpleName() + ":"); 760 pw.print(" "); pw.println(getState().toString()); 761 } 762 } 763