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.RESUMED;
18 import static androidx.lifecycle.Lifecycle.State.STARTED;
19 
20 import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.ACTION_QS_CLICK;
21 import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.ACTION_QS_LONG_PRESS;
22 import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.ACTION_QS_SECONDARY_CLICK;
23 import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.FIELD_IS_FULL_QS;
24 import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.FIELD_QS_POSITION;
25 import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.FIELD_QS_VALUE;
26 import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.FIELD_STATUS_BAR_STATE;
27 import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.TYPE_ACTION;
28 import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
29 
30 import android.annotation.CallSuper;
31 import android.annotation.NonNull;
32 import android.app.ActivityManager;
33 import android.content.Context;
34 import android.content.Intent;
35 import android.graphics.drawable.Drawable;
36 import android.metrics.LogMaker;
37 import android.os.Handler;
38 import android.os.Looper;
39 import android.os.Message;
40 import android.service.quicksettings.Tile;
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.lifecycle.Lifecycle;
47 import androidx.lifecycle.LifecycleOwner;
48 import androidx.lifecycle.LifecycleRegistry;
49 
50 import com.android.internal.annotations.VisibleForTesting;
51 import com.android.internal.logging.InstanceId;
52 import com.android.internal.logging.MetricsLogger;
53 import com.android.internal.logging.UiEventLogger;
54 import com.android.settingslib.RestrictedLockUtils;
55 import com.android.settingslib.RestrictedLockUtilsInternal;
56 import com.android.settingslib.Utils;
57 import com.android.systemui.Dependency;
58 import com.android.systemui.Dumpable;
59 import com.android.systemui.Prefs;
60 import com.android.systemui.plugins.ActivityStarter;
61 import com.android.systemui.plugins.qs.DetailAdapter;
62 import com.android.systemui.plugins.qs.QSIconView;
63 import com.android.systemui.plugins.qs.QSTile;
64 import com.android.systemui.plugins.qs.QSTile.State;
65 import com.android.systemui.plugins.statusbar.StatusBarStateController;
66 import com.android.systemui.qs.PagedTileLayout.TilePage;
67 import com.android.systemui.qs.QSEvent;
68 import com.android.systemui.qs.QSHost;
69 import com.android.systemui.qs.QuickStatusBarHeader;
70 import com.android.systemui.qs.logging.QSLogger;
71 
72 import java.io.FileDescriptor;
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     protected final QSHost mHost;
93     protected final Context mContext;
94     // @NonFinalForTesting
95     protected H mHandler = new H(Dependency.get(Dependency.BG_LOOPER));
96     protected final Handler mUiHandler = new Handler(Looper.getMainLooper());
97     private final ArraySet<Object> mListeners = new ArraySet<>();
98     private final MetricsLogger mMetricsLogger = Dependency.get(MetricsLogger.class);
99     private final StatusBarStateController
100             mStatusBarStateController = Dependency.get(StatusBarStateController.class);
101     private final UiEventLogger mUiEventLogger;
102     private final QSLogger mQSLogger;
103 
104     private final ArrayList<Callback> mCallbacks = new ArrayList<>();
105     private final Object mStaleListener = new Object();
106     protected TState mState;
107     private TState mTmpState;
108     private final InstanceId mInstanceId;
109     private boolean mAnnounceNextStateChange;
110 
111     private String mTileSpec;
112     private EnforcedAdmin mEnforcedAdmin;
113     private boolean mShowingDetail;
114     private int mIsFullQs;
115 
116     private final LifecycleRegistry mLifecycle = new LifecycleRegistry(this);
117 
118     /**
119      * Provides a new {@link TState} of the appropriate type to use between this tile and the
120      * corresponding view.
121      *
122      * @return new state to use by the tile.
123      */
newTileState()124     public abstract TState newTileState();
125 
126     /**
127      * Handles clicks by the user.
128      *
129      * Calls to the controller should be made here to set the new state of the device.
130      */
handleClick()131     abstract protected void handleClick();
132 
133     /**
134      * Update state of the tile based on device state
135      *
136      * Called whenever the state of the tile needs to be updated, either after user
137      * interaction or from callbacks from the controller. It populates {@code state} with the
138      * information to display to the user.
139      *
140      * @param state {@link TState} to populate with information to display
141      * @param arg additional arguments needed to populate {@code state}
142      */
handleUpdateState(TState state, Object arg)143     abstract protected void handleUpdateState(TState state, Object arg);
144 
145     /**
146      * Declare the category of this tile.
147      *
148      * Categories are defined in {@link com.android.internal.logging.nano.MetricsProto.MetricsEvent}
149      * by editing frameworks/base/proto/src/metrics_constants.proto.
150      */
getMetricsCategory()151     abstract public int getMetricsCategory();
152 
QSTileImpl(QSHost host)153     protected QSTileImpl(QSHost host) {
154         mHost = host;
155         mContext = host.getContext();
156         mInstanceId = host.getNewInstanceId();
157         mState = newTileState();
158         mTmpState = newTileState();
159         mQSLogger = host.getQSLogger();
160         mUiEventLogger = host.getUiEventLogger();
161     }
162 
resetStates()163     protected final void resetStates() {
164         mState = newTileState();
165         mTmpState = newTileState();
166     }
167 
168     @NonNull
169     @Override
getLifecycle()170     public Lifecycle getLifecycle() {
171         return mLifecycle;
172     }
173 
174     @Override
getInstanceId()175     public InstanceId getInstanceId() {
176         return mInstanceId;
177     }
178 
179     /**
180      * Adds or removes a listening client for the tile. If the tile has one or more
181      * listening client it will go into the listening state.
182      */
setListening(Object listener, boolean listening)183     public void setListening(Object listener, boolean listening) {
184         mHandler.obtainMessage(H.SET_LISTENING, listening ? 1 : 0, 0, listener).sendToTarget();
185     }
186 
getStaleTimeout()187     protected long getStaleTimeout() {
188         return DEFAULT_STALE_TIMEOUT;
189     }
190 
191     @VisibleForTesting
handleStale()192     protected void handleStale() {
193         setListening(mStaleListener, true);
194     }
195 
getTileSpec()196     public String getTileSpec() {
197         return mTileSpec;
198     }
199 
setTileSpec(String tileSpec)200     public void setTileSpec(String tileSpec) {
201         mTileSpec = tileSpec;
202     }
203 
getHost()204     public QSHost getHost() {
205         return mHost;
206     }
207 
208     /**
209      * Return the {@link QSIconView} to be used by this tile's view.
210      *
211      * @param context view context for the view
212      * @return icon view for this tile
213      */
createTileView(Context context)214     public QSIconView createTileView(Context context) {
215         return new QSIconViewImpl(context);
216     }
217 
getDetailAdapter()218     public DetailAdapter getDetailAdapter() {
219         return null; // optional
220     }
221 
createDetailAdapter()222     protected DetailAdapter createDetailAdapter() {
223         throw new UnsupportedOperationException();
224     }
225 
226     /**
227      * Is a startup check whether this device currently supports this tile.
228      * Should not be used to conditionally hide tiles.  Only checked on tile
229      * creation or whether should be shown in edit screen.
230      */
isAvailable()231     public boolean isAvailable() {
232         return true;
233     }
234 
235     // safe to call from any thread
236 
addCallback(Callback callback)237     public void addCallback(Callback callback) {
238         mHandler.obtainMessage(H.ADD_CALLBACK, callback).sendToTarget();
239     }
240 
removeCallback(Callback callback)241     public void removeCallback(Callback callback) {
242         mHandler.obtainMessage(H.REMOVE_CALLBACK, callback).sendToTarget();
243     }
244 
removeCallbacks()245     public void removeCallbacks() {
246         mHandler.sendEmptyMessage(H.REMOVE_CALLBACKS);
247     }
248 
click()249     public void click() {
250         mMetricsLogger.write(populate(new LogMaker(ACTION_QS_CLICK).setType(TYPE_ACTION)
251                 .addTaggedData(FIELD_STATUS_BAR_STATE,
252                         mStatusBarStateController.getState())));
253         mUiEventLogger.logWithInstanceId(QSEvent.QS_ACTION_CLICK, 0, getMetricsSpec(),
254                 getInstanceId());
255         mQSLogger.logTileClick(mTileSpec, mStatusBarStateController.getState(), mState.state);
256         mHandler.sendEmptyMessage(H.CLICK);
257     }
258 
secondaryClick()259     public void secondaryClick() {
260         mMetricsLogger.write(populate(new LogMaker(ACTION_QS_SECONDARY_CLICK).setType(TYPE_ACTION)
261                 .addTaggedData(FIELD_STATUS_BAR_STATE,
262                         mStatusBarStateController.getState())));
263         mUiEventLogger.logWithInstanceId(QSEvent.QS_ACTION_SECONDARY_CLICK, 0, getMetricsSpec(),
264                 getInstanceId());
265         mQSLogger.logTileSecondaryClick(mTileSpec, mStatusBarStateController.getState(),
266                 mState.state);
267         mHandler.sendEmptyMessage(H.SECONDARY_CLICK);
268     }
269 
longClick()270     public void longClick() {
271         mMetricsLogger.write(populate(new LogMaker(ACTION_QS_LONG_PRESS).setType(TYPE_ACTION)
272                 .addTaggedData(FIELD_STATUS_BAR_STATE,
273                         mStatusBarStateController.getState())));
274         mUiEventLogger.logWithInstanceId(QSEvent.QS_ACTION_LONG_PRESS, 0, getMetricsSpec(),
275                 getInstanceId());
276         mQSLogger.logTileLongClick(mTileSpec, mStatusBarStateController.getState(), mState.state);
277         mHandler.sendEmptyMessage(H.LONG_CLICK);
278 
279         Prefs.putInt(
280                 mContext,
281                 Prefs.Key.QS_LONG_PRESS_TOOLTIP_SHOWN_COUNT,
282                 QuickStatusBarHeader.MAX_TOOLTIP_SHOWN_COUNT);
283     }
284 
populate(LogMaker logMaker)285     public LogMaker populate(LogMaker logMaker) {
286         if (mState instanceof BooleanState) {
287             logMaker.addTaggedData(FIELD_QS_VALUE, ((BooleanState) mState).value ? 1 : 0);
288         }
289         return logMaker.setSubtype(getMetricsCategory())
290                 .addTaggedData(FIELD_IS_FULL_QS, mIsFullQs)
291                 .addTaggedData(FIELD_QS_POSITION, mHost.indexOf(mTileSpec));
292     }
293 
showDetail(boolean show)294     public void showDetail(boolean show) {
295         mHandler.obtainMessage(H.SHOW_DETAIL, show ? 1 : 0, 0).sendToTarget();
296     }
297 
refreshState()298     public void refreshState() {
299         refreshState(null);
300     }
301 
refreshState(Object arg)302     protected final void refreshState(Object arg) {
303         mHandler.obtainMessage(H.REFRESH_STATE, arg).sendToTarget();
304     }
305 
userSwitch(int newUserId)306     public void userSwitch(int newUserId) {
307         mHandler.obtainMessage(H.USER_SWITCH, newUserId, 0).sendToTarget();
308     }
309 
fireToggleStateChanged(boolean state)310     public void fireToggleStateChanged(boolean state) {
311         mHandler.obtainMessage(H.TOGGLE_STATE_CHANGED, state ? 1 : 0, 0).sendToTarget();
312     }
313 
fireScanStateChanged(boolean state)314     public void fireScanStateChanged(boolean state) {
315         mHandler.obtainMessage(H.SCAN_STATE_CHANGED, state ? 1 : 0, 0).sendToTarget();
316     }
317 
destroy()318     public void destroy() {
319         mHandler.sendEmptyMessage(H.DESTROY);
320     }
321 
getState()322     public TState getState() {
323         return mState;
324     }
325 
setDetailListening(boolean listening)326     public void setDetailListening(boolean listening) {
327         // optional
328     }
329 
330     // call only on tile worker looper
331 
handleAddCallback(Callback callback)332     private void handleAddCallback(Callback callback) {
333         mCallbacks.add(callback);
334         callback.onStateChanged(mState);
335     }
336 
handleRemoveCallback(Callback callback)337     private void handleRemoveCallback(Callback callback) {
338         mCallbacks.remove(callback);
339     }
340 
handleRemoveCallbacks()341     private void handleRemoveCallbacks() {
342         mCallbacks.clear();
343     }
344 
345     /**
346      * Handles secondary click on the tile.
347      *
348      * Defaults to {@link QSTileImpl#handleClick}
349      */
handleSecondaryClick()350     protected void handleSecondaryClick() {
351         // Default to normal click.
352         handleClick();
353     }
354 
355     /**
356      * Handles long click on the tile by launching the {@link Intent} defined in
357      * {@link QSTileImpl#getLongClickIntent}
358      */
handleLongClick()359     protected void handleLongClick() {
360         Dependency.get(ActivityStarter.class).postStartActivityDismissingKeyguard(
361                 getLongClickIntent(), 0);
362     }
363 
364     /**
365      * Returns an intent to be launched when the tile is long pressed.
366      *
367      * @return the intent to launch
368      */
getLongClickIntent()369     public abstract Intent getLongClickIntent();
370 
handleRefreshState(Object arg)371     protected void handleRefreshState(Object arg) {
372         handleUpdateState(mTmpState, arg);
373         final boolean changed = mTmpState.copyTo(mState);
374         if (changed) {
375             mQSLogger.logTileUpdated(mTileSpec, mState);
376             handleStateChanged();
377         }
378         mHandler.removeMessages(H.STALE);
379         mHandler.sendEmptyMessageDelayed(H.STALE, getStaleTimeout());
380         setListening(mStaleListener, false);
381     }
382 
handleStateChanged()383     private void handleStateChanged() {
384         boolean delayAnnouncement = shouldAnnouncementBeDelayed();
385         if (mCallbacks.size() != 0) {
386             for (int i = 0; i < mCallbacks.size(); i++) {
387                 mCallbacks.get(i).onStateChanged(mState);
388             }
389             if (mAnnounceNextStateChange && !delayAnnouncement) {
390                 String announcement = composeChangeAnnouncement();
391                 if (announcement != null) {
392                     mCallbacks.get(0).onAnnouncementRequested(announcement);
393                 }
394             }
395         }
396         mAnnounceNextStateChange = mAnnounceNextStateChange && delayAnnouncement;
397     }
398 
shouldAnnouncementBeDelayed()399     protected boolean shouldAnnouncementBeDelayed() {
400         return false;
401     }
402 
composeChangeAnnouncement()403     protected String composeChangeAnnouncement() {
404         return null;
405     }
406 
handleShowDetail(boolean show)407     private void handleShowDetail(boolean show) {
408         mShowingDetail = show;
409         for (int i = 0; i < mCallbacks.size(); i++) {
410             mCallbacks.get(i).onShowDetail(show);
411         }
412     }
413 
isShowingDetail()414     protected boolean isShowingDetail() {
415         return mShowingDetail;
416     }
417 
handleToggleStateChanged(boolean state)418     private void handleToggleStateChanged(boolean state) {
419         for (int i = 0; i < mCallbacks.size(); i++) {
420             mCallbacks.get(i).onToggleStateChanged(state);
421         }
422     }
423 
handleScanStateChanged(boolean state)424     private void handleScanStateChanged(boolean state) {
425         for (int i = 0; i < mCallbacks.size(); i++) {
426             mCallbacks.get(i).onScanStateChanged(state);
427         }
428     }
429 
handleUserSwitch(int newUserId)430     protected void handleUserSwitch(int newUserId) {
431         handleRefreshState(null);
432     }
433 
handleSetListeningInternal(Object listener, boolean listening)434     private void handleSetListeningInternal(Object listener, boolean listening) {
435         // This should be used to go from resumed to paused. Listening for ON_RESUME and ON_PAUSE
436         // in this lifecycle will determine the listening window.
437         if (listening) {
438             if (mListeners.add(listener) && mListeners.size() == 1) {
439                 if (DEBUG) Log.d(TAG, "handleSetListening true");
440                 mLifecycle.setCurrentState(RESUMED);
441                 handleSetListening(listening);
442                 refreshState(); // Ensure we get at least one refresh after listening.
443             }
444         } else {
445             if (mListeners.remove(listener) && mListeners.size() == 0) {
446                 if (DEBUG) Log.d(TAG, "handleSetListening false");
447                 mLifecycle.setCurrentState(STARTED);
448                 handleSetListening(listening);
449             }
450         }
451         updateIsFullQs();
452     }
453 
updateIsFullQs()454     private void updateIsFullQs() {
455         for (Object listener : mListeners) {
456             if (TilePage.class.equals(listener.getClass())) {
457                 mIsFullQs = 1;
458                 return;
459             }
460         }
461         mIsFullQs = 0;
462     }
463 
464     @CallSuper
handleSetListening(boolean listening)465     protected void handleSetListening(boolean listening) {
466         if (mTileSpec != null) {
467             mQSLogger.logTileChangeListening(mTileSpec, listening);
468         }
469     }
470 
handleDestroy()471     protected void handleDestroy() {
472         mQSLogger.logTileDestroyed(mTileSpec, "Handle destroy");
473         if (mListeners.size() != 0) {
474             handleSetListening(false);
475         }
476         mCallbacks.clear();
477         mHandler.removeCallbacksAndMessages(null);
478     }
479 
checkIfRestrictionEnforcedByAdminOnly(State state, String userRestriction)480     protected void checkIfRestrictionEnforcedByAdminOnly(State state, String userRestriction) {
481         EnforcedAdmin admin = RestrictedLockUtilsInternal.checkIfRestrictionEnforced(mContext,
482                 userRestriction, ActivityManager.getCurrentUser());
483         if (admin != null && !RestrictedLockUtilsInternal.hasBaseUserRestriction(mContext,
484                 userRestriction, ActivityManager.getCurrentUser())) {
485             state.disabledByPolicy = true;
486             mEnforcedAdmin = admin;
487         } else {
488             state.disabledByPolicy = false;
489             mEnforcedAdmin = null;
490         }
491     }
492 
493     @Override
getMetricsSpec()494     public String getMetricsSpec() {
495         return mTileSpec;
496     }
497 
498     /**
499      * Provides a default label for the tile.
500      * @return default label for the tile.
501      */
getTileLabel()502     public abstract CharSequence getTileLabel();
503 
getColorForState(Context context, int state)504     public static int getColorForState(Context context, int state) {
505         switch (state) {
506             case Tile.STATE_UNAVAILABLE:
507                 return Utils.getDisabled(context,
508                         Utils.getColorAttrDefaultColor(context, android.R.attr.textColorSecondary));
509             case Tile.STATE_INACTIVE:
510                 return Utils.getColorAttrDefaultColor(context, android.R.attr.textColorSecondary);
511             case Tile.STATE_ACTIVE:
512                 return Utils.getColorAttrDefaultColor(context, android.R.attr.colorPrimary);
513             default:
514                 Log.e("QSTile", "Invalid state " + state);
515                 return 0;
516         }
517     }
518 
519     protected final class H extends Handler {
520         private static final int ADD_CALLBACK = 1;
521         private static final int CLICK = 2;
522         private static final int SECONDARY_CLICK = 3;
523         private static final int LONG_CLICK = 4;
524         private static final int REFRESH_STATE = 5;
525         private static final int SHOW_DETAIL = 6;
526         private static final int USER_SWITCH = 7;
527         private static final int TOGGLE_STATE_CHANGED = 8;
528         private static final int SCAN_STATE_CHANGED = 9;
529         private static final int DESTROY = 10;
530         private static final int REMOVE_CALLBACKS = 11;
531         private static final int REMOVE_CALLBACK = 12;
532         private static final int SET_LISTENING = 13;
533         private static final int STALE = 14;
534 
535         @VisibleForTesting
H(Looper looper)536         protected H(Looper looper) {
537             super(looper);
538         }
539 
540         @Override
handleMessage(Message msg)541         public void handleMessage(Message msg) {
542             String name = null;
543             try {
544                 if (msg.what == ADD_CALLBACK) {
545                     name = "handleAddCallback";
546                     handleAddCallback((QSTile.Callback) msg.obj);
547                 } else if (msg.what == REMOVE_CALLBACKS) {
548                     name = "handleRemoveCallbacks";
549                     handleRemoveCallbacks();
550                 } else if (msg.what == REMOVE_CALLBACK) {
551                     name = "handleRemoveCallback";
552                     handleRemoveCallback((QSTile.Callback) msg.obj);
553                 } else if (msg.what == CLICK) {
554                     name = "handleClick";
555                     if (mState.disabledByPolicy) {
556                         Intent intent = RestrictedLockUtils.getShowAdminSupportDetailsIntent(
557                                 mContext, mEnforcedAdmin);
558                         Dependency.get(ActivityStarter.class).postStartActivityDismissingKeyguard(
559                                 intent, 0);
560                     } else {
561                         handleClick();
562                     }
563                 } else if (msg.what == SECONDARY_CLICK) {
564                     name = "handleSecondaryClick";
565                     handleSecondaryClick();
566                 } else if (msg.what == LONG_CLICK) {
567                     name = "handleLongClick";
568                     handleLongClick();
569                 } else if (msg.what == REFRESH_STATE) {
570                     name = "handleRefreshState";
571                     handleRefreshState(msg.obj);
572                 } else if (msg.what == SHOW_DETAIL) {
573                     name = "handleShowDetail";
574                     handleShowDetail(msg.arg1 != 0);
575                 } else if (msg.what == USER_SWITCH) {
576                     name = "handleUserSwitch";
577                     handleUserSwitch(msg.arg1);
578                 } else if (msg.what == TOGGLE_STATE_CHANGED) {
579                     name = "handleToggleStateChanged";
580                     handleToggleStateChanged(msg.arg1 != 0);
581                 } else if (msg.what == SCAN_STATE_CHANGED) {
582                     name = "handleScanStateChanged";
583                     handleScanStateChanged(msg.arg1 != 0);
584                 } else if (msg.what == DESTROY) {
585                     name = "handleDestroy";
586                     handleDestroy();
587                 } else if (msg.what == SET_LISTENING) {
588                     name = "handleSetListeningInternal";
589                     handleSetListeningInternal(msg.obj, msg.arg1 != 0);
590                 } else if (msg.what == STALE) {
591                     name = "handleStale";
592                     handleStale();
593                 } else {
594                     throw new IllegalArgumentException("Unknown msg: " + msg.what);
595                 }
596             } catch (Throwable t) {
597                 final String error = "Error in " + name;
598                 Log.w(TAG, error, t);
599                 mHost.warn(error, t);
600             }
601         }
602     }
603 
604     public static class DrawableIcon extends Icon {
605         protected final Drawable mDrawable;
606         protected final Drawable mInvisibleDrawable;
607 
DrawableIcon(Drawable drawable)608         public DrawableIcon(Drawable drawable) {
609             mDrawable = drawable;
610             mInvisibleDrawable = drawable.getConstantState().newDrawable();
611         }
612 
613         @Override
getDrawable(Context context)614         public Drawable getDrawable(Context context) {
615             return mDrawable;
616         }
617 
618         @Override
getInvisibleDrawable(Context context)619         public Drawable getInvisibleDrawable(Context context) {
620             return mInvisibleDrawable;
621         }
622 
623         @Override
624         @NonNull
toString()625         public String toString() {
626             return "DrawableIcon";
627         }
628     }
629 
630     public static class DrawableIconWithRes extends DrawableIcon {
631         private final int mId;
632 
DrawableIconWithRes(Drawable drawable, int id)633         public DrawableIconWithRes(Drawable drawable, int id) {
634             super(drawable);
635             mId = id;
636         }
637 
638         @Override
equals(Object o)639         public boolean equals(Object o) {
640             return o instanceof DrawableIconWithRes && ((DrawableIconWithRes) o).mId == mId;
641         }
642 
643         @Override
644         @NonNull
toString()645         public String toString() {
646             return String.format("DrawableIconWithRes[resId=0x%08x]", mId);
647         }
648     }
649 
650     public static class ResourceIcon extends Icon {
651         private static final SparseArray<Icon> ICONS = new SparseArray<Icon>();
652 
653         protected final int mResId;
654 
ResourceIcon(int resId)655         private ResourceIcon(int resId) {
656             mResId = resId;
657         }
658 
get(int resId)659         public static synchronized Icon get(int resId) {
660             Icon icon = ICONS.get(resId);
661             if (icon == null) {
662                 icon = new ResourceIcon(resId);
663                 ICONS.put(resId, icon);
664             }
665             return icon;
666         }
667 
668         @Override
getDrawable(Context context)669         public Drawable getDrawable(Context context) {
670             return context.getDrawable(mResId);
671         }
672 
673         @Override
getInvisibleDrawable(Context context)674         public Drawable getInvisibleDrawable(Context context) {
675             return context.getDrawable(mResId);
676         }
677 
678         @Override
equals(Object o)679         public boolean equals(Object o) {
680             return o instanceof ResourceIcon && ((ResourceIcon) o).mResId == mResId;
681         }
682 
683         @Override
684         @NonNull
toString()685         public String toString() {
686             return String.format("ResourceIcon[resId=0x%08x]", mResId);
687         }
688     }
689 
690     protected static class AnimationIcon extends ResourceIcon {
691         private final int mAnimatedResId;
692 
AnimationIcon(int resId, int staticResId)693         public AnimationIcon(int resId, int staticResId) {
694             super(staticResId);
695             mAnimatedResId = resId;
696         }
697 
698         @Override
getDrawable(Context context)699         public Drawable getDrawable(Context context) {
700             // workaround: get a clean state for every new AVD
701             return context.getDrawable(mAnimatedResId).getConstantState().newDrawable();
702         }
703 
704         @Override
705         @NonNull
toString()706         public String toString() {
707             return String.format("AnimationIcon[resId=0x%08x]", mResId);
708         }
709     }
710 
711     /**
712      * Dumps the state of this tile along with its name.
713      *
714      * This may be used for CTS testing of tiles.
715      */
716     @Override
dump(FileDescriptor fd, PrintWriter pw, String[] args)717     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
718         pw.println(this.getClass().getSimpleName() + ":");
719         pw.print("    "); pw.println(getState().toString());
720     }
721 }
722