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