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