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