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.DetailAdapter;
64 import com.android.systemui.plugins.qs.QSIconView;
65 import com.android.systemui.plugins.qs.QSTile;
66 import com.android.systemui.plugins.qs.QSTile.State;
67 import com.android.systemui.plugins.statusbar.StatusBarStateController;
68 import com.android.systemui.qs.QSEvent;
69 import com.android.systemui.qs.QSHost;
70 import com.android.systemui.qs.SideLabelTileLayout;
71 import com.android.systemui.qs.logging.QSLogger;
72 
73 import java.io.FileDescriptor;
74 import java.io.PrintWriter;
75 import java.util.ArrayList;
76 
77 /**
78  * Base quick-settings tile, extend this to create a new tile.
79  *
80  * State management done on a looper provided by the host.  Tiles should update state in
81  * handleUpdateState.  Callbacks affecting state should use refreshState to trigger another
82  * state update pass on tile looper.
83  *
84  * @param <TState> see above
85  */
86 public abstract class QSTileImpl<TState extends State> implements QSTile, LifecycleOwner, Dumpable {
87     protected final String TAG = "Tile." + getClass().getSimpleName();
88     protected static final boolean DEBUG = Log.isLoggable("Tile", Log.DEBUG);
89 
90     private static final long DEFAULT_STALE_TIMEOUT = 10 * DateUtils.MINUTE_IN_MILLIS;
91     protected static final Object ARG_SHOW_TRANSIENT_ENABLING = new Object();
92 
93     private static final int READY_STATE_NOT_READY = 0;
94     private static final int READY_STATE_READYING = 1;
95     private static final int READY_STATE_READY = 2;
96 
97     protected final QSHost mHost;
98     protected final Context mContext;
99     // @NonFinalForTesting
100     protected final H mHandler;
101     protected final Handler mUiHandler;
102     private final ArraySet<Object> mListeners = new ArraySet<>();
103     private final MetricsLogger mMetricsLogger;
104     private final StatusBarStateController mStatusBarStateController;
105     protected final ActivityStarter mActivityStarter;
106     private final UiEventLogger mUiEventLogger;
107     private final FalsingManager mFalsingManager;
108     private final QSLogger mQSLogger;
109     private volatile int mReadyState;
110 
111     private final ArrayList<Callback> mCallbacks = new ArrayList<>();
112     private final Object mStaleListener = new Object();
113     protected TState mState;
114     private TState mTmpState;
115     private final InstanceId mInstanceId;
116     private boolean mAnnounceNextStateChange;
117 
118     private String mTileSpec;
119     private EnforcedAdmin mEnforcedAdmin;
120     private boolean mShowingDetail;
121     private int mIsFullQs;
122 
123     private final LifecycleRegistry mLifecycle = new LifecycleRegistry(this);
124 
125     /**
126      * Provides a new {@link TState} of the appropriate type to use between this tile and the
127      * corresponding view.
128      *
129      * @return new state to use by the tile.
130      */
newTileState()131     public abstract TState newTileState();
132 
133     /**
134      * Handles clicks by the user.
135      *
136      * Calls to the controller should be made here to set the new state of the device.
137      *
138      * @param view The view that was clicked.
139      */
handleClick(@ullable View view)140     protected abstract void handleClick(@Nullable View view);
141 
142     /**
143      * Update state of the tile based on device state
144      *
145      * Called whenever the state of the tile needs to be updated, either after user
146      * interaction or from callbacks from the controller. It populates {@code state} with the
147      * information to display to the user.
148      *
149      * @param state {@link TState} to populate with information to display
150      * @param arg additional arguments needed to populate {@code state}
151      */
handleUpdateState(TState state, Object arg)152     abstract protected void handleUpdateState(TState state, Object arg);
153 
154     /**
155      * Declare the category of this tile.
156      *
157      * Categories are defined in {@link com.android.internal.logging.nano.MetricsProto.MetricsEvent}
158      * by editing frameworks/base/proto/src/metrics_constants.proto.
159      */
getMetricsCategory()160     abstract public int getMetricsCategory();
161 
162     /**
163      * Performs initialization of the tile
164      *
165      * Use this to perform initialization of the tile. Empty by default.
166      */
handleInitialize()167     protected void handleInitialize() {
168 
169     }
170 
QSTileImpl( QSHost host, Looper backgroundLooper, Handler mainHandler, FalsingManager falsingManager, MetricsLogger metricsLogger, StatusBarStateController statusBarStateController, ActivityStarter activityStarter, QSLogger qsLogger )171     protected QSTileImpl(
172             QSHost host,
173             Looper backgroundLooper,
174             Handler mainHandler,
175             FalsingManager falsingManager,
176             MetricsLogger metricsLogger,
177             StatusBarStateController statusBarStateController,
178             ActivityStarter activityStarter,
179             QSLogger qsLogger
180     ) {
181         mHost = host;
182         mContext = host.getContext();
183         mInstanceId = host.getNewInstanceId();
184         mUiEventLogger = host.getUiEventLogger();
185 
186         mUiHandler = mainHandler;
187         mHandler = new H(backgroundLooper);
188         mFalsingManager = falsingManager;
189         mQSLogger = qsLogger;
190         mMetricsLogger = metricsLogger;
191         mStatusBarStateController = statusBarStateController;
192         mActivityStarter = activityStarter;
193 
194         resetStates();
195         mUiHandler.post(() -> mLifecycle.setCurrentState(CREATED));
196     }
197 
resetStates()198     protected final void resetStates() {
199         mState = newTileState();
200         mTmpState = newTileState();
201         mState.spec = mTileSpec;
202         mTmpState.spec = mTileSpec;
203     }
204 
205     @NonNull
206     @Override
getLifecycle()207     public Lifecycle getLifecycle() {
208         return mLifecycle;
209     }
210 
211     @Override
getInstanceId()212     public InstanceId getInstanceId() {
213         return mInstanceId;
214     }
215 
216     /**
217      * Adds or removes a listening client for the tile. If the tile has one or more
218      * listening client it will go into the listening state.
219      */
setListening(Object listener, boolean listening)220     public void setListening(Object listener, boolean listening) {
221         mHandler.obtainMessage(H.SET_LISTENING, listening ? 1 : 0, 0, listener).sendToTarget();
222     }
223 
getStaleTimeout()224     protected long getStaleTimeout() {
225         return DEFAULT_STALE_TIMEOUT;
226     }
227 
228     @VisibleForTesting
handleStale()229     protected void handleStale() {
230         setListening(mStaleListener, true);
231     }
232 
getTileSpec()233     public String getTileSpec() {
234         return mTileSpec;
235     }
236 
setTileSpec(String tileSpec)237     public void setTileSpec(String tileSpec) {
238         mTileSpec = tileSpec;
239         mState.spec = tileSpec;
240         mTmpState.spec = tileSpec;
241     }
242 
getHost()243     public QSHost getHost() {
244         return mHost;
245     }
246 
247     /**
248      * Return the {@link QSIconView} to be used by this tile's view.
249      *
250      * @param context view context for the view
251      * @return icon view for this tile
252      */
createTileView(Context context)253     public QSIconView createTileView(Context context) {
254         return new QSIconViewImpl(context);
255     }
256 
getDetailAdapter()257     public DetailAdapter getDetailAdapter() {
258         return null; // optional
259     }
260 
createDetailAdapter()261     protected DetailAdapter createDetailAdapter() {
262         throw new UnsupportedOperationException();
263     }
264 
265     /**
266      * Is a startup check whether this device currently supports this tile.
267      * Should not be used to conditionally hide tiles.  Only checked on tile
268      * creation or whether should be shown in edit screen.
269      */
isAvailable()270     public boolean isAvailable() {
271         return true;
272     }
273 
274     // safe to call from any thread
275 
addCallback(Callback callback)276     public void addCallback(Callback callback) {
277         mHandler.obtainMessage(H.ADD_CALLBACK, callback).sendToTarget();
278     }
279 
removeCallback(Callback callback)280     public void removeCallback(Callback callback) {
281         mHandler.obtainMessage(H.REMOVE_CALLBACK, callback).sendToTarget();
282     }
283 
removeCallbacks()284     public void removeCallbacks() {
285         mHandler.sendEmptyMessage(H.REMOVE_CALLBACKS);
286     }
287 
click(@ullable View view)288     public void click(@Nullable View view) {
289         mMetricsLogger.write(populate(new LogMaker(ACTION_QS_CLICK).setType(TYPE_ACTION)
290                 .addTaggedData(FIELD_STATUS_BAR_STATE,
291                         mStatusBarStateController.getState())));
292         mUiEventLogger.logWithInstanceId(QSEvent.QS_ACTION_CLICK, 0, getMetricsSpec(),
293                 getInstanceId());
294         mQSLogger.logTileClick(mTileSpec, mStatusBarStateController.getState(), mState.state);
295         if (!mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
296             mHandler.obtainMessage(H.CLICK, view).sendToTarget();
297         }
298     }
299 
secondaryClick(@ullable View view)300     public void secondaryClick(@Nullable View view) {
301         mMetricsLogger.write(populate(new LogMaker(ACTION_QS_SECONDARY_CLICK).setType(TYPE_ACTION)
302                 .addTaggedData(FIELD_STATUS_BAR_STATE,
303                         mStatusBarStateController.getState())));
304         mUiEventLogger.logWithInstanceId(QSEvent.QS_ACTION_SECONDARY_CLICK, 0, getMetricsSpec(),
305                 getInstanceId());
306         mQSLogger.logTileSecondaryClick(mTileSpec, mStatusBarStateController.getState(),
307                 mState.state);
308         mHandler.obtainMessage(H.SECONDARY_CLICK, view).sendToTarget();
309     }
310 
311     @Override
longClick(@ullable View view)312     public void longClick(@Nullable View view) {
313         mMetricsLogger.write(populate(new LogMaker(ACTION_QS_LONG_PRESS).setType(TYPE_ACTION)
314                 .addTaggedData(FIELD_STATUS_BAR_STATE,
315                         mStatusBarStateController.getState())));
316         mUiEventLogger.logWithInstanceId(QSEvent.QS_ACTION_LONG_PRESS, 0, getMetricsSpec(),
317                 getInstanceId());
318         mQSLogger.logTileLongClick(mTileSpec, mStatusBarStateController.getState(), mState.state);
319         mHandler.obtainMessage(H.LONG_CLICK, view).sendToTarget();
320     }
321 
populate(LogMaker logMaker)322     public LogMaker populate(LogMaker logMaker) {
323         if (mState instanceof BooleanState) {
324             logMaker.addTaggedData(FIELD_QS_VALUE, ((BooleanState) mState).value ? 1 : 0);
325         }
326         return logMaker.setSubtype(getMetricsCategory())
327                 .addTaggedData(FIELD_IS_FULL_QS, mIsFullQs)
328                 .addTaggedData(FIELD_QS_POSITION, mHost.indexOf(mTileSpec));
329     }
330 
showDetail(boolean show)331     public void showDetail(boolean show) {
332         mHandler.obtainMessage(H.SHOW_DETAIL, show ? 1 : 0, 0).sendToTarget();
333     }
334 
refreshState()335     public void refreshState() {
336         refreshState(null);
337     }
338 
refreshState(Object arg)339     protected final void refreshState(Object arg) {
340         mHandler.obtainMessage(H.REFRESH_STATE, arg).sendToTarget();
341     }
342 
userSwitch(int newUserId)343     public void userSwitch(int newUserId) {
344         mHandler.obtainMessage(H.USER_SWITCH, newUserId, 0).sendToTarget();
345     }
346 
fireToggleStateChanged(boolean state)347     public void fireToggleStateChanged(boolean state) {
348         mHandler.obtainMessage(H.TOGGLE_STATE_CHANGED, state ? 1 : 0, 0).sendToTarget();
349     }
350 
fireScanStateChanged(boolean state)351     public void fireScanStateChanged(boolean state) {
352         mHandler.obtainMessage(H.SCAN_STATE_CHANGED, state ? 1 : 0, 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 view The view that was clicked.
404      */
handleSecondaryClick(@ullable View view)405     protected void handleSecondaryClick(@Nullable View view) {
406         // Default to normal click.
407         handleClick(view);
408     }
409 
410     /**
411      * Handles long click on the tile by launching the {@link Intent} defined in
412      * {@link QSTileImpl#getLongClickIntent}.
413      *
414      * @param view The view from which the opening window will be animated.
415      */
handleLongClick(@ullable View view)416     protected void handleLongClick(@Nullable View view) {
417         ActivityLaunchAnimator.Controller animationController =
418                 view != null ? ActivityLaunchAnimator.Controller.fromView(view,
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      */
getLongClickIntent()429     public abstract Intent getLongClickIntent();
430 
handleRefreshState(Object arg)431     protected void handleRefreshState(Object arg) {
432         handleUpdateState(mTmpState, arg);
433         boolean changed = mTmpState.copyTo(mState);
434         if (mReadyState == READY_STATE_READYING) {
435             mReadyState = READY_STATE_READY;
436             changed = true;
437         }
438         if (changed) {
439             mQSLogger.logTileUpdated(mTileSpec, mState);
440             handleStateChanged();
441         }
442         mHandler.removeMessages(H.STALE);
443         mHandler.sendEmptyMessageDelayed(H.STALE, getStaleTimeout());
444         setListening(mStaleListener, false);
445     }
446 
handleStateChanged()447     private void handleStateChanged() {
448         boolean delayAnnouncement = shouldAnnouncementBeDelayed();
449         if (mCallbacks.size() != 0) {
450             for (int i = 0; i < mCallbacks.size(); i++) {
451                 mCallbacks.get(i).onStateChanged(mState);
452             }
453             if (mAnnounceNextStateChange && !delayAnnouncement) {
454                 String announcement = composeChangeAnnouncement();
455                 if (announcement != null) {
456                     mCallbacks.get(0).onAnnouncementRequested(announcement);
457                 }
458             }
459         }
460         mAnnounceNextStateChange = mAnnounceNextStateChange && delayAnnouncement;
461     }
462 
shouldAnnouncementBeDelayed()463     protected boolean shouldAnnouncementBeDelayed() {
464         return false;
465     }
466 
composeChangeAnnouncement()467     protected String composeChangeAnnouncement() {
468         return null;
469     }
470 
handleShowDetail(boolean show)471     private void handleShowDetail(boolean show) {
472         mShowingDetail = show;
473         for (int i = 0; i < mCallbacks.size(); i++) {
474             mCallbacks.get(i).onShowDetail(show);
475         }
476     }
477 
isShowingDetail()478     protected boolean isShowingDetail() {
479         return mShowingDetail;
480     }
481 
handleToggleStateChanged(boolean state)482     private void handleToggleStateChanged(boolean state) {
483         for (int i = 0; i < mCallbacks.size(); i++) {
484             mCallbacks.get(i).onToggleStateChanged(state);
485         }
486     }
487 
handleScanStateChanged(boolean state)488     private void handleScanStateChanged(boolean state) {
489         for (int i = 0; i < mCallbacks.size(); i++) {
490             mCallbacks.get(i).onScanStateChanged(state);
491         }
492     }
493 
handleUserSwitch(int newUserId)494     protected void handleUserSwitch(int newUserId) {
495         handleRefreshState(null);
496     }
497 
handleSetListeningInternal(Object listener, boolean listening)498     private void handleSetListeningInternal(Object listener, boolean listening) {
499         // This should be used to go from resumed to paused. Listening for ON_RESUME and ON_PAUSE
500         // in this lifecycle will determine the listening window.
501         if (listening) {
502             if (mListeners.add(listener) && mListeners.size() == 1) {
503                 if (DEBUG) Log.d(TAG, "handleSetListening true");
504                 handleSetListening(listening);
505                 mUiHandler.post(() -> {
506                     // This tile has been destroyed, the state should not change anymore and we
507                     // should not refresh it anymore.
508                     if (mLifecycle.getCurrentState().equals(DESTROYED)) return;
509                     mLifecycle.setCurrentState(RESUMED);
510                     if (mReadyState == READY_STATE_NOT_READY) {
511                         mReadyState = READY_STATE_READYING;
512                     }
513                     refreshState(); // Ensure we get at least one refresh after listening.
514                 });
515             }
516         } else {
517             if (mListeners.remove(listener) && mListeners.size() == 0) {
518                 if (DEBUG) Log.d(TAG, "handleSetListening false");
519                 handleSetListening(listening);
520                 mUiHandler.post(() -> {
521                     // This tile has been destroyed, the state should not change anymore.
522                     if (mLifecycle.getCurrentState().equals(DESTROYED)) return;
523                     mLifecycle.setCurrentState(STARTED);
524                 });
525             }
526         }
527         updateIsFullQs();
528     }
529 
updateIsFullQs()530     private void updateIsFullQs() {
531         for (Object listener : mListeners) {
532             if (SideLabelTileLayout.class.equals(listener.getClass())) {
533                 mIsFullQs = 1;
534                 return;
535             }
536         }
537         mIsFullQs = 0;
538     }
539 
540     @CallSuper
handleSetListening(boolean listening)541     protected void handleSetListening(boolean listening) {
542         if (mTileSpec != null) {
543             mQSLogger.logTileChangeListening(mTileSpec, listening);
544         }
545     }
546 
handleDestroy()547     protected void handleDestroy() {
548         mQSLogger.logTileDestroyed(mTileSpec, "Handle destroy");
549         if (mListeners.size() != 0) {
550             handleSetListening(false);
551             mListeners.clear();
552         }
553         mCallbacks.clear();
554         mHandler.removeCallbacksAndMessages(null);
555         // This will force it to be removed from all controllers that may have it registered.
556         mUiHandler.post(() -> {
557             mLifecycle.setCurrentState(DESTROYED);
558         });
559     }
560 
checkIfRestrictionEnforcedByAdminOnly(State state, String userRestriction)561     protected void checkIfRestrictionEnforcedByAdminOnly(State state, String userRestriction) {
562         EnforcedAdmin admin = RestrictedLockUtilsInternal.checkIfRestrictionEnforced(mContext,
563                 userRestriction, mHost.getUserId());
564         if (admin != null && !RestrictedLockUtilsInternal.hasBaseUserRestriction(mContext,
565                 userRestriction, mHost.getUserId())) {
566             state.disabledByPolicy = true;
567             mEnforcedAdmin = admin;
568         } else {
569             state.disabledByPolicy = false;
570             mEnforcedAdmin = null;
571         }
572     }
573 
574     @Override
getMetricsSpec()575     public String getMetricsSpec() {
576         return mTileSpec;
577     }
578 
579     /**
580      * Provides a default label for the tile.
581      * @return default label for the tile.
582      */
getTileLabel()583     public abstract CharSequence getTileLabel();
584 
585     /**
586      * @return {@code true} if the tile has refreshed state at least once after having set its
587      *         lifecycle to {@link Lifecycle.State#RESUMED}.
588      */
589     @Override
isTileReady()590     public boolean isTileReady() {
591         return mReadyState == READY_STATE_READY;
592     }
593 
594     protected final class H extends Handler {
595         private static final int ADD_CALLBACK = 1;
596         private static final int CLICK = 2;
597         private static final int SECONDARY_CLICK = 3;
598         private static final int LONG_CLICK = 4;
599         private static final int REFRESH_STATE = 5;
600         private static final int SHOW_DETAIL = 6;
601         private static final int USER_SWITCH = 7;
602         private static final int TOGGLE_STATE_CHANGED = 8;
603         private static final int SCAN_STATE_CHANGED = 9;
604         private static final int DESTROY = 10;
605         private static final int REMOVE_CALLBACKS = 11;
606         private static final int REMOVE_CALLBACK = 12;
607         private static final int SET_LISTENING = 13;
608         @VisibleForTesting
609         protected static final int STALE = 14;
610         private static final int INITIALIZE = 15;
611 
612         @VisibleForTesting
H(Looper looper)613         protected H(Looper looper) {
614             super(looper);
615         }
616 
617         @Override
handleMessage(Message msg)618         public void handleMessage(Message msg) {
619             String name = null;
620             try {
621                 if (msg.what == ADD_CALLBACK) {
622                     name = "handleAddCallback";
623                     handleAddCallback((QSTile.Callback) msg.obj);
624                 } else if (msg.what == REMOVE_CALLBACKS) {
625                     name = "handleRemoveCallbacks";
626                     handleRemoveCallbacks();
627                 } else if (msg.what == REMOVE_CALLBACK) {
628                     name = "handleRemoveCallback";
629                     handleRemoveCallback((QSTile.Callback) msg.obj);
630                 } else if (msg.what == CLICK) {
631                     name = "handleClick";
632                     if (mState.disabledByPolicy) {
633                         Intent intent = RestrictedLockUtils.getShowAdminSupportDetailsIntent(
634                                 mContext, mEnforcedAdmin);
635                         mActivityStarter.postStartActivityDismissingKeyguard(intent, 0);
636                     } else {
637                         handleClick((View) msg.obj);
638                     }
639                 } else if (msg.what == SECONDARY_CLICK) {
640                     name = "handleSecondaryClick";
641                     handleSecondaryClick((View) msg.obj);
642                 } else if (msg.what == LONG_CLICK) {
643                     name = "handleLongClick";
644                     handleLongClick((View) msg.obj);
645                 } else if (msg.what == REFRESH_STATE) {
646                     name = "handleRefreshState";
647                     handleRefreshState(msg.obj);
648                 } else if (msg.what == SHOW_DETAIL) {
649                     name = "handleShowDetail";
650                     handleShowDetail(msg.arg1 != 0);
651                 } else if (msg.what == USER_SWITCH) {
652                     name = "handleUserSwitch";
653                     handleUserSwitch(msg.arg1);
654                 } else if (msg.what == TOGGLE_STATE_CHANGED) {
655                     name = "handleToggleStateChanged";
656                     handleToggleStateChanged(msg.arg1 != 0);
657                 } else if (msg.what == SCAN_STATE_CHANGED) {
658                     name = "handleScanStateChanged";
659                     handleScanStateChanged(msg.arg1 != 0);
660                 } else if (msg.what == DESTROY) {
661                     name = "handleDestroy";
662                     handleDestroy();
663                 } else if (msg.what == SET_LISTENING) {
664                     name = "handleSetListeningInternal";
665                     handleSetListeningInternal(msg.obj, msg.arg1 != 0);
666                 } else if (msg.what == STALE) {
667                     name = "handleStale";
668                     handleStale();
669                 } else if (msg.what == INITIALIZE) {
670                     name = "initialize";
671                     handleInitialize();
672                 } else {
673                     throw new IllegalArgumentException("Unknown msg: " + msg.what);
674                 }
675             } catch (Throwable t) {
676                 final String error = "Error in " + name;
677                 Log.w(TAG, error, t);
678                 mHost.warn(error, t);
679             }
680         }
681     }
682 
683     public static class DrawableIcon extends Icon {
684         protected final Drawable mDrawable;
685         protected final Drawable mInvisibleDrawable;
686 
DrawableIcon(Drawable drawable)687         public DrawableIcon(Drawable drawable) {
688             mDrawable = drawable;
689             mInvisibleDrawable = drawable.getConstantState().newDrawable();
690         }
691 
692         @Override
getDrawable(Context context)693         public Drawable getDrawable(Context context) {
694             return mDrawable;
695         }
696 
697         @Override
getInvisibleDrawable(Context context)698         public Drawable getInvisibleDrawable(Context context) {
699             return mInvisibleDrawable;
700         }
701 
702         @Override
703         @NonNull
toString()704         public String toString() {
705             return "DrawableIcon";
706         }
707     }
708 
709     public static class DrawableIconWithRes extends DrawableIcon {
710         private final int mId;
711 
DrawableIconWithRes(Drawable drawable, int id)712         public DrawableIconWithRes(Drawable drawable, int id) {
713             super(drawable);
714             mId = id;
715         }
716 
717         @Override
equals(Object o)718         public boolean equals(Object o) {
719             return o instanceof DrawableIconWithRes && ((DrawableIconWithRes) o).mId == mId;
720         }
721 
722         @Override
723         @NonNull
toString()724         public String toString() {
725             return String.format("DrawableIconWithRes[resId=0x%08x]", mId);
726         }
727     }
728 
729     public static class ResourceIcon extends Icon {
730         private static final SparseArray<Icon> ICONS = new SparseArray<Icon>();
731 
732         protected final int mResId;
733 
ResourceIcon(int resId)734         private ResourceIcon(int resId) {
735             mResId = resId;
736         }
737 
get(int resId)738         public static synchronized Icon get(int resId) {
739             Icon icon = ICONS.get(resId);
740             if (icon == null) {
741                 icon = new ResourceIcon(resId);
742                 ICONS.put(resId, icon);
743             }
744             return icon;
745         }
746 
747         @Override
getDrawable(Context context)748         public Drawable getDrawable(Context context) {
749             return context.getDrawable(mResId);
750         }
751 
752         @Override
getInvisibleDrawable(Context context)753         public Drawable getInvisibleDrawable(Context context) {
754             return context.getDrawable(mResId);
755         }
756 
757         @Override
equals(Object o)758         public boolean equals(Object o) {
759             return o instanceof ResourceIcon && ((ResourceIcon) o).mResId == mResId;
760         }
761 
762         @Override
763         @NonNull
toString()764         public String toString() {
765             return String.format("ResourceIcon[resId=0x%08x]", mResId);
766         }
767     }
768 
769     protected static class AnimationIcon extends ResourceIcon {
770         private final int mAnimatedResId;
771 
AnimationIcon(int resId, int staticResId)772         public AnimationIcon(int resId, int staticResId) {
773             super(staticResId);
774             mAnimatedResId = resId;
775         }
776 
777         @Override
getDrawable(Context context)778         public Drawable getDrawable(Context context) {
779             // workaround: get a clean state for every new AVD
780             return context.getDrawable(mAnimatedResId).getConstantState().newDrawable();
781         }
782 
783         @Override
784         @NonNull
toString()785         public String toString() {
786             return String.format("AnimationIcon[resId=0x%08x]", mResId);
787         }
788     }
789 
790     /**
791      * Dumps the state of this tile along with its name.
792      *
793      * This may be used for CTS testing of tiles.
794      */
795     @Override
dump(FileDescriptor fd, PrintWriter pw, String[] args)796     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
797         pw.println(this.getClass().getSimpleName() + ":");
798         pw.print("    "); pw.println(getState().toString());
799     }
800 }
801