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.RESUMED; 18 import static androidx.lifecycle.Lifecycle.State.STARTED; 19 20 import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.ACTION_QS_CLICK; 21 import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.ACTION_QS_LONG_PRESS; 22 import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.ACTION_QS_SECONDARY_CLICK; 23 import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.FIELD_IS_FULL_QS; 24 import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.FIELD_QS_POSITION; 25 import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.FIELD_QS_VALUE; 26 import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.FIELD_STATUS_BAR_STATE; 27 import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.TYPE_ACTION; 28 import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; 29 30 import android.annotation.CallSuper; 31 import android.annotation.NonNull; 32 import android.app.ActivityManager; 33 import android.content.Context; 34 import android.content.Intent; 35 import android.graphics.drawable.Drawable; 36 import android.metrics.LogMaker; 37 import android.os.Handler; 38 import android.os.Looper; 39 import android.os.Message; 40 import android.service.quicksettings.Tile; 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.lifecycle.Lifecycle; 47 import androidx.lifecycle.LifecycleOwner; 48 import androidx.lifecycle.LifecycleRegistry; 49 50 import com.android.internal.annotations.VisibleForTesting; 51 import com.android.internal.logging.InstanceId; 52 import com.android.internal.logging.MetricsLogger; 53 import com.android.internal.logging.UiEventLogger; 54 import com.android.settingslib.RestrictedLockUtils; 55 import com.android.settingslib.RestrictedLockUtilsInternal; 56 import com.android.settingslib.Utils; 57 import com.android.systemui.Dependency; 58 import com.android.systemui.Dumpable; 59 import com.android.systemui.Prefs; 60 import com.android.systemui.plugins.ActivityStarter; 61 import com.android.systemui.plugins.qs.DetailAdapter; 62 import com.android.systemui.plugins.qs.QSIconView; 63 import com.android.systemui.plugins.qs.QSTile; 64 import com.android.systemui.plugins.qs.QSTile.State; 65 import com.android.systemui.plugins.statusbar.StatusBarStateController; 66 import com.android.systemui.qs.PagedTileLayout.TilePage; 67 import com.android.systemui.qs.QSEvent; 68 import com.android.systemui.qs.QSHost; 69 import com.android.systemui.qs.QuickStatusBarHeader; 70 import com.android.systemui.qs.logging.QSLogger; 71 72 import java.io.FileDescriptor; 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 protected final QSHost mHost; 93 protected final Context mContext; 94 // @NonFinalForTesting 95 protected H mHandler = new H(Dependency.get(Dependency.BG_LOOPER)); 96 protected final Handler mUiHandler = new Handler(Looper.getMainLooper()); 97 private final ArraySet<Object> mListeners = new ArraySet<>(); 98 private final MetricsLogger mMetricsLogger = Dependency.get(MetricsLogger.class); 99 private final StatusBarStateController 100 mStatusBarStateController = Dependency.get(StatusBarStateController.class); 101 private final UiEventLogger mUiEventLogger; 102 private final QSLogger mQSLogger; 103 104 private final ArrayList<Callback> mCallbacks = new ArrayList<>(); 105 private final Object mStaleListener = new Object(); 106 protected TState mState; 107 private TState mTmpState; 108 private final InstanceId mInstanceId; 109 private boolean mAnnounceNextStateChange; 110 111 private String mTileSpec; 112 private EnforcedAdmin mEnforcedAdmin; 113 private boolean mShowingDetail; 114 private int mIsFullQs; 115 116 private final LifecycleRegistry mLifecycle = new LifecycleRegistry(this); 117 118 /** 119 * Provides a new {@link TState} of the appropriate type to use between this tile and the 120 * corresponding view. 121 * 122 * @return new state to use by the tile. 123 */ newTileState()124 public abstract TState newTileState(); 125 126 /** 127 * Handles clicks by the user. 128 * 129 * Calls to the controller should be made here to set the new state of the device. 130 */ handleClick()131 abstract protected void handleClick(); 132 133 /** 134 * Update state of the tile based on device state 135 * 136 * Called whenever the state of the tile needs to be updated, either after user 137 * interaction or from callbacks from the controller. It populates {@code state} with the 138 * information to display to the user. 139 * 140 * @param state {@link TState} to populate with information to display 141 * @param arg additional arguments needed to populate {@code state} 142 */ handleUpdateState(TState state, Object arg)143 abstract protected void handleUpdateState(TState state, Object arg); 144 145 /** 146 * Declare the category of this tile. 147 * 148 * Categories are defined in {@link com.android.internal.logging.nano.MetricsProto.MetricsEvent} 149 * by editing frameworks/base/proto/src/metrics_constants.proto. 150 */ getMetricsCategory()151 abstract public int getMetricsCategory(); 152 QSTileImpl(QSHost host)153 protected QSTileImpl(QSHost host) { 154 mHost = host; 155 mContext = host.getContext(); 156 mInstanceId = host.getNewInstanceId(); 157 mState = newTileState(); 158 mTmpState = newTileState(); 159 mQSLogger = host.getQSLogger(); 160 mUiEventLogger = host.getUiEventLogger(); 161 } 162 resetStates()163 protected final void resetStates() { 164 mState = newTileState(); 165 mTmpState = newTileState(); 166 } 167 168 @NonNull 169 @Override getLifecycle()170 public Lifecycle getLifecycle() { 171 return mLifecycle; 172 } 173 174 @Override getInstanceId()175 public InstanceId getInstanceId() { 176 return mInstanceId; 177 } 178 179 /** 180 * Adds or removes a listening client for the tile. If the tile has one or more 181 * listening client it will go into the listening state. 182 */ setListening(Object listener, boolean listening)183 public void setListening(Object listener, boolean listening) { 184 mHandler.obtainMessage(H.SET_LISTENING, listening ? 1 : 0, 0, listener).sendToTarget(); 185 } 186 getStaleTimeout()187 protected long getStaleTimeout() { 188 return DEFAULT_STALE_TIMEOUT; 189 } 190 191 @VisibleForTesting handleStale()192 protected void handleStale() { 193 setListening(mStaleListener, true); 194 } 195 getTileSpec()196 public String getTileSpec() { 197 return mTileSpec; 198 } 199 setTileSpec(String tileSpec)200 public void setTileSpec(String tileSpec) { 201 mTileSpec = tileSpec; 202 } 203 getHost()204 public QSHost getHost() { 205 return mHost; 206 } 207 208 /** 209 * Return the {@link QSIconView} to be used by this tile's view. 210 * 211 * @param context view context for the view 212 * @return icon view for this tile 213 */ createTileView(Context context)214 public QSIconView createTileView(Context context) { 215 return new QSIconViewImpl(context); 216 } 217 getDetailAdapter()218 public DetailAdapter getDetailAdapter() { 219 return null; // optional 220 } 221 createDetailAdapter()222 protected DetailAdapter createDetailAdapter() { 223 throw new UnsupportedOperationException(); 224 } 225 226 /** 227 * Is a startup check whether this device currently supports this tile. 228 * Should not be used to conditionally hide tiles. Only checked on tile 229 * creation or whether should be shown in edit screen. 230 */ isAvailable()231 public boolean isAvailable() { 232 return true; 233 } 234 235 // safe to call from any thread 236 addCallback(Callback callback)237 public void addCallback(Callback callback) { 238 mHandler.obtainMessage(H.ADD_CALLBACK, callback).sendToTarget(); 239 } 240 removeCallback(Callback callback)241 public void removeCallback(Callback callback) { 242 mHandler.obtainMessage(H.REMOVE_CALLBACK, callback).sendToTarget(); 243 } 244 removeCallbacks()245 public void removeCallbacks() { 246 mHandler.sendEmptyMessage(H.REMOVE_CALLBACKS); 247 } 248 click()249 public void click() { 250 mMetricsLogger.write(populate(new LogMaker(ACTION_QS_CLICK).setType(TYPE_ACTION) 251 .addTaggedData(FIELD_STATUS_BAR_STATE, 252 mStatusBarStateController.getState()))); 253 mUiEventLogger.logWithInstanceId(QSEvent.QS_ACTION_CLICK, 0, getMetricsSpec(), 254 getInstanceId()); 255 mQSLogger.logTileClick(mTileSpec, mStatusBarStateController.getState(), mState.state); 256 mHandler.sendEmptyMessage(H.CLICK); 257 } 258 secondaryClick()259 public void secondaryClick() { 260 mMetricsLogger.write(populate(new LogMaker(ACTION_QS_SECONDARY_CLICK).setType(TYPE_ACTION) 261 .addTaggedData(FIELD_STATUS_BAR_STATE, 262 mStatusBarStateController.getState()))); 263 mUiEventLogger.logWithInstanceId(QSEvent.QS_ACTION_SECONDARY_CLICK, 0, getMetricsSpec(), 264 getInstanceId()); 265 mQSLogger.logTileSecondaryClick(mTileSpec, mStatusBarStateController.getState(), 266 mState.state); 267 mHandler.sendEmptyMessage(H.SECONDARY_CLICK); 268 } 269 longClick()270 public void longClick() { 271 mMetricsLogger.write(populate(new LogMaker(ACTION_QS_LONG_PRESS).setType(TYPE_ACTION) 272 .addTaggedData(FIELD_STATUS_BAR_STATE, 273 mStatusBarStateController.getState()))); 274 mUiEventLogger.logWithInstanceId(QSEvent.QS_ACTION_LONG_PRESS, 0, getMetricsSpec(), 275 getInstanceId()); 276 mQSLogger.logTileLongClick(mTileSpec, mStatusBarStateController.getState(), mState.state); 277 mHandler.sendEmptyMessage(H.LONG_CLICK); 278 279 Prefs.putInt( 280 mContext, 281 Prefs.Key.QS_LONG_PRESS_TOOLTIP_SHOWN_COUNT, 282 QuickStatusBarHeader.MAX_TOOLTIP_SHOWN_COUNT); 283 } 284 populate(LogMaker logMaker)285 public LogMaker populate(LogMaker logMaker) { 286 if (mState instanceof BooleanState) { 287 logMaker.addTaggedData(FIELD_QS_VALUE, ((BooleanState) mState).value ? 1 : 0); 288 } 289 return logMaker.setSubtype(getMetricsCategory()) 290 .addTaggedData(FIELD_IS_FULL_QS, mIsFullQs) 291 .addTaggedData(FIELD_QS_POSITION, mHost.indexOf(mTileSpec)); 292 } 293 showDetail(boolean show)294 public void showDetail(boolean show) { 295 mHandler.obtainMessage(H.SHOW_DETAIL, show ? 1 : 0, 0).sendToTarget(); 296 } 297 refreshState()298 public void refreshState() { 299 refreshState(null); 300 } 301 refreshState(Object arg)302 protected final void refreshState(Object arg) { 303 mHandler.obtainMessage(H.REFRESH_STATE, arg).sendToTarget(); 304 } 305 userSwitch(int newUserId)306 public void userSwitch(int newUserId) { 307 mHandler.obtainMessage(H.USER_SWITCH, newUserId, 0).sendToTarget(); 308 } 309 fireToggleStateChanged(boolean state)310 public void fireToggleStateChanged(boolean state) { 311 mHandler.obtainMessage(H.TOGGLE_STATE_CHANGED, state ? 1 : 0, 0).sendToTarget(); 312 } 313 fireScanStateChanged(boolean state)314 public void fireScanStateChanged(boolean state) { 315 mHandler.obtainMessage(H.SCAN_STATE_CHANGED, state ? 1 : 0, 0).sendToTarget(); 316 } 317 destroy()318 public void destroy() { 319 mHandler.sendEmptyMessage(H.DESTROY); 320 } 321 getState()322 public TState getState() { 323 return mState; 324 } 325 setDetailListening(boolean listening)326 public void setDetailListening(boolean listening) { 327 // optional 328 } 329 330 // call only on tile worker looper 331 handleAddCallback(Callback callback)332 private void handleAddCallback(Callback callback) { 333 mCallbacks.add(callback); 334 callback.onStateChanged(mState); 335 } 336 handleRemoveCallback(Callback callback)337 private void handleRemoveCallback(Callback callback) { 338 mCallbacks.remove(callback); 339 } 340 handleRemoveCallbacks()341 private void handleRemoveCallbacks() { 342 mCallbacks.clear(); 343 } 344 345 /** 346 * Handles secondary click on the tile. 347 * 348 * Defaults to {@link QSTileImpl#handleClick} 349 */ handleSecondaryClick()350 protected void handleSecondaryClick() { 351 // Default to normal click. 352 handleClick(); 353 } 354 355 /** 356 * Handles long click on the tile by launching the {@link Intent} defined in 357 * {@link QSTileImpl#getLongClickIntent} 358 */ handleLongClick()359 protected void handleLongClick() { 360 Dependency.get(ActivityStarter.class).postStartActivityDismissingKeyguard( 361 getLongClickIntent(), 0); 362 } 363 364 /** 365 * Returns an intent to be launched when the tile is long pressed. 366 * 367 * @return the intent to launch 368 */ getLongClickIntent()369 public abstract Intent getLongClickIntent(); 370 handleRefreshState(Object arg)371 protected void handleRefreshState(Object arg) { 372 handleUpdateState(mTmpState, arg); 373 final boolean changed = mTmpState.copyTo(mState); 374 if (changed) { 375 mQSLogger.logTileUpdated(mTileSpec, mState); 376 handleStateChanged(); 377 } 378 mHandler.removeMessages(H.STALE); 379 mHandler.sendEmptyMessageDelayed(H.STALE, getStaleTimeout()); 380 setListening(mStaleListener, false); 381 } 382 handleStateChanged()383 private void handleStateChanged() { 384 boolean delayAnnouncement = shouldAnnouncementBeDelayed(); 385 if (mCallbacks.size() != 0) { 386 for (int i = 0; i < mCallbacks.size(); i++) { 387 mCallbacks.get(i).onStateChanged(mState); 388 } 389 if (mAnnounceNextStateChange && !delayAnnouncement) { 390 String announcement = composeChangeAnnouncement(); 391 if (announcement != null) { 392 mCallbacks.get(0).onAnnouncementRequested(announcement); 393 } 394 } 395 } 396 mAnnounceNextStateChange = mAnnounceNextStateChange && delayAnnouncement; 397 } 398 shouldAnnouncementBeDelayed()399 protected boolean shouldAnnouncementBeDelayed() { 400 return false; 401 } 402 composeChangeAnnouncement()403 protected String composeChangeAnnouncement() { 404 return null; 405 } 406 handleShowDetail(boolean show)407 private void handleShowDetail(boolean show) { 408 mShowingDetail = show; 409 for (int i = 0; i < mCallbacks.size(); i++) { 410 mCallbacks.get(i).onShowDetail(show); 411 } 412 } 413 isShowingDetail()414 protected boolean isShowingDetail() { 415 return mShowingDetail; 416 } 417 handleToggleStateChanged(boolean state)418 private void handleToggleStateChanged(boolean state) { 419 for (int i = 0; i < mCallbacks.size(); i++) { 420 mCallbacks.get(i).onToggleStateChanged(state); 421 } 422 } 423 handleScanStateChanged(boolean state)424 private void handleScanStateChanged(boolean state) { 425 for (int i = 0; i < mCallbacks.size(); i++) { 426 mCallbacks.get(i).onScanStateChanged(state); 427 } 428 } 429 handleUserSwitch(int newUserId)430 protected void handleUserSwitch(int newUserId) { 431 handleRefreshState(null); 432 } 433 handleSetListeningInternal(Object listener, boolean listening)434 private void handleSetListeningInternal(Object listener, boolean listening) { 435 // This should be used to go from resumed to paused. Listening for ON_RESUME and ON_PAUSE 436 // in this lifecycle will determine the listening window. 437 if (listening) { 438 if (mListeners.add(listener) && mListeners.size() == 1) { 439 if (DEBUG) Log.d(TAG, "handleSetListening true"); 440 mLifecycle.setCurrentState(RESUMED); 441 handleSetListening(listening); 442 refreshState(); // Ensure we get at least one refresh after listening. 443 } 444 } else { 445 if (mListeners.remove(listener) && mListeners.size() == 0) { 446 if (DEBUG) Log.d(TAG, "handleSetListening false"); 447 mLifecycle.setCurrentState(STARTED); 448 handleSetListening(listening); 449 } 450 } 451 updateIsFullQs(); 452 } 453 updateIsFullQs()454 private void updateIsFullQs() { 455 for (Object listener : mListeners) { 456 if (TilePage.class.equals(listener.getClass())) { 457 mIsFullQs = 1; 458 return; 459 } 460 } 461 mIsFullQs = 0; 462 } 463 464 @CallSuper handleSetListening(boolean listening)465 protected void handleSetListening(boolean listening) { 466 if (mTileSpec != null) { 467 mQSLogger.logTileChangeListening(mTileSpec, listening); 468 } 469 } 470 handleDestroy()471 protected void handleDestroy() { 472 mQSLogger.logTileDestroyed(mTileSpec, "Handle destroy"); 473 if (mListeners.size() != 0) { 474 handleSetListening(false); 475 } 476 mCallbacks.clear(); 477 mHandler.removeCallbacksAndMessages(null); 478 } 479 checkIfRestrictionEnforcedByAdminOnly(State state, String userRestriction)480 protected void checkIfRestrictionEnforcedByAdminOnly(State state, String userRestriction) { 481 EnforcedAdmin admin = RestrictedLockUtilsInternal.checkIfRestrictionEnforced(mContext, 482 userRestriction, ActivityManager.getCurrentUser()); 483 if (admin != null && !RestrictedLockUtilsInternal.hasBaseUserRestriction(mContext, 484 userRestriction, ActivityManager.getCurrentUser())) { 485 state.disabledByPolicy = true; 486 mEnforcedAdmin = admin; 487 } else { 488 state.disabledByPolicy = false; 489 mEnforcedAdmin = null; 490 } 491 } 492 493 @Override getMetricsSpec()494 public String getMetricsSpec() { 495 return mTileSpec; 496 } 497 498 /** 499 * Provides a default label for the tile. 500 * @return default label for the tile. 501 */ getTileLabel()502 public abstract CharSequence getTileLabel(); 503 getColorForState(Context context, int state)504 public static int getColorForState(Context context, int state) { 505 switch (state) { 506 case Tile.STATE_UNAVAILABLE: 507 return Utils.getDisabled(context, 508 Utils.getColorAttrDefaultColor(context, android.R.attr.textColorSecondary)); 509 case Tile.STATE_INACTIVE: 510 return Utils.getColorAttrDefaultColor(context, android.R.attr.textColorSecondary); 511 case Tile.STATE_ACTIVE: 512 return Utils.getColorAttrDefaultColor(context, android.R.attr.colorPrimary); 513 default: 514 Log.e("QSTile", "Invalid state " + state); 515 return 0; 516 } 517 } 518 519 protected final class H extends Handler { 520 private static final int ADD_CALLBACK = 1; 521 private static final int CLICK = 2; 522 private static final int SECONDARY_CLICK = 3; 523 private static final int LONG_CLICK = 4; 524 private static final int REFRESH_STATE = 5; 525 private static final int SHOW_DETAIL = 6; 526 private static final int USER_SWITCH = 7; 527 private static final int TOGGLE_STATE_CHANGED = 8; 528 private static final int SCAN_STATE_CHANGED = 9; 529 private static final int DESTROY = 10; 530 private static final int REMOVE_CALLBACKS = 11; 531 private static final int REMOVE_CALLBACK = 12; 532 private static final int SET_LISTENING = 13; 533 private static final int STALE = 14; 534 535 @VisibleForTesting H(Looper looper)536 protected H(Looper looper) { 537 super(looper); 538 } 539 540 @Override handleMessage(Message msg)541 public void handleMessage(Message msg) { 542 String name = null; 543 try { 544 if (msg.what == ADD_CALLBACK) { 545 name = "handleAddCallback"; 546 handleAddCallback((QSTile.Callback) msg.obj); 547 } else if (msg.what == REMOVE_CALLBACKS) { 548 name = "handleRemoveCallbacks"; 549 handleRemoveCallbacks(); 550 } else if (msg.what == REMOVE_CALLBACK) { 551 name = "handleRemoveCallback"; 552 handleRemoveCallback((QSTile.Callback) msg.obj); 553 } else if (msg.what == CLICK) { 554 name = "handleClick"; 555 if (mState.disabledByPolicy) { 556 Intent intent = RestrictedLockUtils.getShowAdminSupportDetailsIntent( 557 mContext, mEnforcedAdmin); 558 Dependency.get(ActivityStarter.class).postStartActivityDismissingKeyguard( 559 intent, 0); 560 } else { 561 handleClick(); 562 } 563 } else if (msg.what == SECONDARY_CLICK) { 564 name = "handleSecondaryClick"; 565 handleSecondaryClick(); 566 } else if (msg.what == LONG_CLICK) { 567 name = "handleLongClick"; 568 handleLongClick(); 569 } else if (msg.what == REFRESH_STATE) { 570 name = "handleRefreshState"; 571 handleRefreshState(msg.obj); 572 } else if (msg.what == SHOW_DETAIL) { 573 name = "handleShowDetail"; 574 handleShowDetail(msg.arg1 != 0); 575 } else if (msg.what == USER_SWITCH) { 576 name = "handleUserSwitch"; 577 handleUserSwitch(msg.arg1); 578 } else if (msg.what == TOGGLE_STATE_CHANGED) { 579 name = "handleToggleStateChanged"; 580 handleToggleStateChanged(msg.arg1 != 0); 581 } else if (msg.what == SCAN_STATE_CHANGED) { 582 name = "handleScanStateChanged"; 583 handleScanStateChanged(msg.arg1 != 0); 584 } else if (msg.what == DESTROY) { 585 name = "handleDestroy"; 586 handleDestroy(); 587 } else if (msg.what == SET_LISTENING) { 588 name = "handleSetListeningInternal"; 589 handleSetListeningInternal(msg.obj, msg.arg1 != 0); 590 } else if (msg.what == STALE) { 591 name = "handleStale"; 592 handleStale(); 593 } else { 594 throw new IllegalArgumentException("Unknown msg: " + msg.what); 595 } 596 } catch (Throwable t) { 597 final String error = "Error in " + name; 598 Log.w(TAG, error, t); 599 mHost.warn(error, t); 600 } 601 } 602 } 603 604 public static class DrawableIcon extends Icon { 605 protected final Drawable mDrawable; 606 protected final Drawable mInvisibleDrawable; 607 DrawableIcon(Drawable drawable)608 public DrawableIcon(Drawable drawable) { 609 mDrawable = drawable; 610 mInvisibleDrawable = drawable.getConstantState().newDrawable(); 611 } 612 613 @Override getDrawable(Context context)614 public Drawable getDrawable(Context context) { 615 return mDrawable; 616 } 617 618 @Override getInvisibleDrawable(Context context)619 public Drawable getInvisibleDrawable(Context context) { 620 return mInvisibleDrawable; 621 } 622 623 @Override 624 @NonNull toString()625 public String toString() { 626 return "DrawableIcon"; 627 } 628 } 629 630 public static class DrawableIconWithRes extends DrawableIcon { 631 private final int mId; 632 DrawableIconWithRes(Drawable drawable, int id)633 public DrawableIconWithRes(Drawable drawable, int id) { 634 super(drawable); 635 mId = id; 636 } 637 638 @Override equals(Object o)639 public boolean equals(Object o) { 640 return o instanceof DrawableIconWithRes && ((DrawableIconWithRes) o).mId == mId; 641 } 642 643 @Override 644 @NonNull toString()645 public String toString() { 646 return String.format("DrawableIconWithRes[resId=0x%08x]", mId); 647 } 648 } 649 650 public static class ResourceIcon extends Icon { 651 private static final SparseArray<Icon> ICONS = new SparseArray<Icon>(); 652 653 protected final int mResId; 654 ResourceIcon(int resId)655 private ResourceIcon(int resId) { 656 mResId = resId; 657 } 658 get(int resId)659 public static synchronized Icon get(int resId) { 660 Icon icon = ICONS.get(resId); 661 if (icon == null) { 662 icon = new ResourceIcon(resId); 663 ICONS.put(resId, icon); 664 } 665 return icon; 666 } 667 668 @Override getDrawable(Context context)669 public Drawable getDrawable(Context context) { 670 return context.getDrawable(mResId); 671 } 672 673 @Override getInvisibleDrawable(Context context)674 public Drawable getInvisibleDrawable(Context context) { 675 return context.getDrawable(mResId); 676 } 677 678 @Override equals(Object o)679 public boolean equals(Object o) { 680 return o instanceof ResourceIcon && ((ResourceIcon) o).mResId == mResId; 681 } 682 683 @Override 684 @NonNull toString()685 public String toString() { 686 return String.format("ResourceIcon[resId=0x%08x]", mResId); 687 } 688 } 689 690 protected static class AnimationIcon extends ResourceIcon { 691 private final int mAnimatedResId; 692 AnimationIcon(int resId, int staticResId)693 public AnimationIcon(int resId, int staticResId) { 694 super(staticResId); 695 mAnimatedResId = resId; 696 } 697 698 @Override getDrawable(Context context)699 public Drawable getDrawable(Context context) { 700 // workaround: get a clean state for every new AVD 701 return context.getDrawable(mAnimatedResId).getConstantState().newDrawable(); 702 } 703 704 @Override 705 @NonNull toString()706 public String toString() { 707 return String.format("AnimationIcon[resId=0x%08x]", mResId); 708 } 709 } 710 711 /** 712 * Dumps the state of this tile along with its name. 713 * 714 * This may be used for CTS testing of tiles. 715 */ 716 @Override dump(FileDescriptor fd, PrintWriter pw, String[] args)717 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 718 pw.println(this.getClass().getSimpleName() + ":"); 719 pw.print(" "); pw.println(getState().toString()); 720 } 721 } 722