• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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