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