/*
 * Copyright (C) 2017 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
 * except in compliance with the License. You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the
 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the specific language governing
 * permissions and limitations under the License.
 */

package com.android.systemui.qs.tileimpl;

import static androidx.lifecycle.Lifecycle.State.DESTROYED;
import static androidx.lifecycle.Lifecycle.State.RESUMED;

import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.ACTION_QS_CLICK;
import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.ACTION_QS_LONG_PRESS;
import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.ACTION_QS_SECONDARY_CLICK;
import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.FIELD_IS_FULL_QS;
import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.FIELD_QS_POSITION;
import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.FIELD_QS_VALUE;
import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.FIELD_STATUS_BAR_STATE;
import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.TYPE_ACTION;
import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;

import android.app.ActivityManager;
import android.content.Context;
import android.content.res.ColorUtils;
import android.content.res.Configuration;
import android.content.Intent;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.metrics.LogMaker;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.Vibrator;
import android.os.UserHandle;
import android.content.res.ColorUtils;
import android.provider.Settings;
import android.provider.Settings.System;
import android.service.quicksettings.Tile;
import android.text.format.DateUtils;
import android.util.ArraySet;
import android.util.Log;
import android.util.SparseArray;

import androidx.annotation.NonNull;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LifecycleRegistry;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.logging.MetricsLogger;
import com.android.settingslib.RestrictedLockUtils;
import com.android.settingslib.RestrictedLockUtilsInternal;
import com.android.settingslib.Utils;
import com.android.systemui.Dependency;
import com.android.systemui.Dumpable;
import com.android.systemui.Prefs;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.plugins.qs.DetailAdapter;
import com.android.systemui.plugins.qs.QSIconView;
import com.android.systemui.plugins.qs.QSTile;
import com.android.systemui.plugins.qs.QSTile.State;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.qs.PagedTileLayout.TilePage;
import com.android.systemui.qs.QSHost;
import com.android.systemui.qs.QuickStatusBarHeader;
import com.android.systemui.tuner.TunerService;

import com.android.systemui.R;

import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Random;

/**
 * Base quick-settings tile, extend this to create a new tile.
 *
 * State management done on a looper provided by the host.  Tiles should update state in
 * handleUpdateState.  Callbacks affecting state should use refreshState to trigger another
 * state update pass on tile looper.
 *
 * @param <TState> see above
 */
public abstract class QSTileImpl<TState extends State> implements QSTile, LifecycleOwner, Dumpable, TunerService.Tunable {
    protected final String TAG = "Tile." + getClass().getSimpleName();
    protected static final boolean DEBUG = Log.isLoggable("Tile", Log.DEBUG);

    private static final long DEFAULT_STALE_TIMEOUT = 10 * DateUtils.MINUTE_IN_MILLIS;
    protected static final Object ARG_SHOW_TRANSIENT_ENABLING = new Object();

    protected final QSHost mHost;
    protected final Context mContext;
    // @NonFinalForTesting
    protected H mHandler = new H(Dependency.get(Dependency.BG_LOOPER));
    protected final Handler mUiHandler = new Handler(Looper.getMainLooper());
    private final ArraySet<Object> mListeners = new ArraySet<>();
    private final MetricsLogger mMetricsLogger = Dependency.get(MetricsLogger.class);
    private final StatusBarStateController
            mStatusBarStateController = Dependency.get(StatusBarStateController.class);

    private final ArrayList<Callback> mCallbacks = new ArrayList<>();
    private final Object mStaleListener = new Object();
    protected TState mState = newTileState();
    private TState mTmpState = newTileState();
    private boolean mAnnounceNextStateChange;

    private String mTileSpec;
    private EnforcedAdmin mEnforcedAdmin;
    private boolean mShowingDetail;
    private int mIsFullQs;

    protected Vibrator mVibrator;
    private boolean mVibrationEnabled;

    private static final String QUICK_SETTINGS_VIBRATE =
            Settings.Secure.QUICK_SETTINGS_VIBRATE;

    private final LifecycleRegistry mLifecycle = new LifecycleRegistry(this);

    public abstract TState newTileState();

    abstract protected void handleClick();

    abstract protected void handleUpdateState(TState state, Object arg);

    @Override
    public boolean isDualTarget() {
        return false;
    }

    /**
     * Declare the category of this tile.
     *
     * Categories are defined in {@link com.android.internal.logging.nano.MetricsProto.MetricsEvent}
     * by editing frameworks/base/proto/src/metrics_constants.proto.
     */
    abstract public int getMetricsCategory();

    protected QSTileImpl(QSHost host) {
        mHost = host;
        mContext = host.getContext();
        mVibrator = (Vibrator) mContext.getSystemService(Context.VIBRATOR_SERVICE);

        Dependency.get(TunerService.class).addTunable(this,
                QUICK_SETTINGS_VIBRATE);
    }

    @Override
    public void onTuningChanged(String key, String newValue) {
        switch (key) {
            case QUICK_SETTINGS_VIBRATE:
                mVibrationEnabled =
                        TunerService.parseIntegerSwitch(newValue, false);
                break;
            default:
                break;
        }
    }

    @NonNull
    @Override
    public Lifecycle getLifecycle() {
        return mLifecycle;
    }

    /**
     * Adds or removes a listening client for the tile. If the tile has one or more
     * listening client it will go into the listening state.
     */
    public void setListening(Object listener, boolean listening) {
        mHandler.obtainMessage(H.SET_LISTENING, listening ? 1 : 0, 0, listener).sendToTarget();
    }

    protected long getStaleTimeout() {
        return DEFAULT_STALE_TIMEOUT;
    }

    @VisibleForTesting
    protected void handleStale() {
        setListening(mStaleListener, true);
    }

    public String getTileSpec() {
        return mTileSpec;
    }

    public void setTileSpec(String tileSpec) {
        mTileSpec = tileSpec;
    }

    public QSHost getHost() {
        return mHost;
    }

    public QSIconView createTileView(Context context) {
        return new QSIconViewImpl(context);
    }

    public DetailAdapter getDetailAdapter() {
        return null; // optional
    }

    protected DetailAdapter createDetailAdapter() {
        throw new UnsupportedOperationException();
    }

    /**
     * Is a startup check whether this device currently supports this tile.
     * Should not be used to conditionally hide tiles.  Only checked on tile
     * creation or whether should be shown in edit screen.
     */
    public boolean isAvailable() {
        return true;
    }

    public void vibrateTile(int duration) {
        if (mVibrationEnabled && mVibrator != null && mVibrator.hasVibrator()) {
            mVibrator.vibrate(duration);
        }
    }

    // safe to call from any thread

    public void addCallback(Callback callback) {
        mHandler.obtainMessage(H.ADD_CALLBACK, callback).sendToTarget();
    }

    public void removeCallback(Callback callback) {
        mHandler.obtainMessage(H.REMOVE_CALLBACK, callback).sendToTarget();
    }

    public void removeCallbacks() {
        mHandler.sendEmptyMessage(H.REMOVE_CALLBACKS);
    }

    public void click() {
        mMetricsLogger.write(populate(new LogMaker(ACTION_QS_CLICK).setType(TYPE_ACTION)
                .addTaggedData(FIELD_STATUS_BAR_STATE,
                        mStatusBarStateController.getState())));
        mHandler.sendEmptyMessage(H.CLICK);
        vibrateTile(100);
    }

    public void secondaryClick() {
        mMetricsLogger.write(populate(new LogMaker(ACTION_QS_SECONDARY_CLICK).setType(TYPE_ACTION)
                .addTaggedData(FIELD_STATUS_BAR_STATE,
                        mStatusBarStateController.getState())));
        mHandler.sendEmptyMessage(H.SECONDARY_CLICK);
        vibrateTile(100);
    }

    public void longClick() {
        mMetricsLogger.write(populate(new LogMaker(ACTION_QS_LONG_PRESS).setType(TYPE_ACTION)
                .addTaggedData(FIELD_STATUS_BAR_STATE,
                        mStatusBarStateController.getState())));
        mHandler.sendEmptyMessage(H.LONG_CLICK);
        vibrateTile(100);

        Prefs.putInt(
                mContext,
                Prefs.Key.QS_LONG_PRESS_TOOLTIP_SHOWN_COUNT,
                QuickStatusBarHeader.MAX_TOOLTIP_SHOWN_COUNT);
    }

    public LogMaker populate(LogMaker logMaker) {
        if (mState instanceof BooleanState) {
            logMaker.addTaggedData(FIELD_QS_VALUE, ((BooleanState) mState).value ? 1 : 0);
        }
        return logMaker.setSubtype(getMetricsCategory())
                .addTaggedData(FIELD_IS_FULL_QS, mIsFullQs)
                .addTaggedData(FIELD_QS_POSITION, mHost.indexOf(mTileSpec));
    }

    public void showDetail(boolean show) {
        mHandler.obtainMessage(H.SHOW_DETAIL, show ? 1 : 0, 0).sendToTarget();
    }

    public void refreshState() {
        refreshState(null);
    }

    protected final void refreshState(Object arg) {
        mHandler.obtainMessage(H.REFRESH_STATE, arg).sendToTarget();
    }

    public void userSwitch(int newUserId) {
        mHandler.obtainMessage(H.USER_SWITCH, newUserId, 0).sendToTarget();
    }

    public void fireToggleStateChanged(boolean state) {
        mHandler.obtainMessage(H.TOGGLE_STATE_CHANGED, state ? 1 : 0, 0).sendToTarget();
    }

    public void fireScanStateChanged(boolean state) {
        mHandler.obtainMessage(H.SCAN_STATE_CHANGED, state ? 1 : 0, 0).sendToTarget();
    }

    public void destroy() {
        mHandler.sendEmptyMessage(H.DESTROY);
    }

    public TState getState() {
        return mState;
    }

    public void setDetailListening(boolean listening) {
        // optional
    }

    // call only on tile worker looper

    private void handleAddCallback(Callback callback) {
        mCallbacks.add(callback);
        callback.onStateChanged(mState);
    }

    private void handleRemoveCallback(Callback callback) {
        mCallbacks.remove(callback);
    }

    private void handleRemoveCallbacks() {
        mCallbacks.clear();
    }

    protected void handleSecondaryClick() {
        // Default to normal click.
        handleClick();
    }

    protected void handleLongClick() {
        if (getLongClickIntent() != null) {
            Dependency.get(ActivityStarter.class).postStartActivityDismissingKeyguard(
                    getLongClickIntent(), 0);
        }
    }

    public abstract Intent getLongClickIntent();

    protected void handleRefreshState(Object arg) {
        handleUpdateState(mTmpState, arg);
        final boolean changed = mTmpState.copyTo(mState);
        if (changed) {
            handleStateChanged();
        }
        mHandler.removeMessages(H.STALE);
        mHandler.sendEmptyMessageDelayed(H.STALE, getStaleTimeout());
        setListening(mStaleListener, false);
    }

    private void handleStateChanged() {
        boolean delayAnnouncement = shouldAnnouncementBeDelayed();
        if (mCallbacks.size() != 0) {
            for (int i = 0; i < mCallbacks.size(); i++) {
                mCallbacks.get(i).onStateChanged(mState);
            }
            if (mAnnounceNextStateChange && !delayAnnouncement) {
                String announcement = composeChangeAnnouncement();
                if (announcement != null) {
                    mCallbacks.get(0).onAnnouncementRequested(announcement);
                }
            }
        }
        mAnnounceNextStateChange = mAnnounceNextStateChange && delayAnnouncement;
    }

    protected boolean shouldAnnouncementBeDelayed() {
        return false;
    }

    protected String composeChangeAnnouncement() {
        return null;
    }

    private void handleShowDetail(boolean show) {
        mShowingDetail = show;
        for (int i = 0; i < mCallbacks.size(); i++) {
            mCallbacks.get(i).onShowDetail(show);
        }
    }

    protected boolean isShowingDetail() {
        return mShowingDetail;
    }

    private void handleToggleStateChanged(boolean state) {
        for (int i = 0; i < mCallbacks.size(); i++) {
            mCallbacks.get(i).onToggleStateChanged(state);
        }
    }

    private void handleScanStateChanged(boolean state) {
        for (int i = 0; i < mCallbacks.size(); i++) {
            mCallbacks.get(i).onScanStateChanged(state);
        }
    }

    protected void handleUserSwitch(int newUserId) {
        handleRefreshState(null);
    }

    private void handleSetListeningInternal(Object listener, boolean listening) {
        if (listening) {
            if (mListeners.add(listener) && mListeners.size() == 1) {
                if (DEBUG) Log.d(TAG, "handleSetListening true");
                mLifecycle.markState(RESUMED);
                handleSetListening(listening);
                refreshState(); // Ensure we get at least one refresh after listening.
            }
        } else {
            if (mListeners.remove(listener) && mListeners.size() == 0) {
                if (DEBUG) Log.d(TAG, "handleSetListening false");
                mLifecycle.markState(DESTROYED);
                handleSetListening(listening);
            }
        }
        updateIsFullQs();
    }

    private void updateIsFullQs() {
        for (Object listener : mListeners) {
            if (TilePage.class.equals(listener.getClass())) {
                mIsFullQs = 1;
                return;
            }
        }
        mIsFullQs = 0;
    }

    protected abstract void handleSetListening(boolean listening);

    protected void handleDestroy() {
        if (mListeners.size() != 0) {
            handleSetListening(false);
        }
        mCallbacks.clear();
    }

    protected void checkIfRestrictionEnforcedByAdminOnly(State state, String userRestriction) {
        EnforcedAdmin admin = RestrictedLockUtilsInternal.checkIfRestrictionEnforced(mContext,
                userRestriction, ActivityManager.getCurrentUser());
        if (admin != null && !RestrictedLockUtilsInternal.hasBaseUserRestriction(mContext,
                userRestriction, ActivityManager.getCurrentUser())) {
            state.disabledByPolicy = true;
            mEnforcedAdmin = admin;
        } else {
            state.disabledByPolicy = false;
            mEnforcedAdmin = null;
        }
    }

    public abstract CharSequence getTileLabel();

    public static int getColorForState(Context context, int state) {
        int useQSAccentTint = Settings.System.getIntForUser(context.getContentResolver(),
                Settings.System.QS_TILE_ACCENT_TINT, 0, UserHandle.USER_CURRENT) ;
        boolean qsIconPrimary = Settings.System.getIntForUser(context.getContentResolver(),
                    Settings.System.QS_TILE_ICON_PRIMARY, 0, UserHandle.USER_CURRENT) == 1;

        boolean rgbTint = Settings.System.getIntForUser(context.getContentResolver(),
                    Settings.System.QS_TILE_RGB_TINT, 0, UserHandle.USER_CURRENT) == 1;
        int qsTileStyle = Settings.System.getIntForUser(context.getContentResolver(),
                    Settings.System.QS_TILE_STYLE, 0, UserHandle.USER_CURRENT);
        boolean tintgradient = Settings.System.getIntForUser(context.getContentResolver(),
                Settings.System.QS_TILE_GRADIENT, 0, UserHandle.USER_CURRENT) == 1;
        boolean isgradient = (tintgradient && qsTileStyle == 0);
        switch (state) {
            case Tile.STATE_UNAVAILABLE:
                return Utils.getDisabled(context,
                        Utils.getColorAttrDefaultColor(context, android.R.attr.textColorSecondary));
            case Tile.STATE_INACTIVE:
                return Utils.getColorAttrDefaultColor(context, android.R.attr.textColorSecondary);
            case Tile.STATE_ACTIVE:
                if (useQSAccentTint == 1) {
                    return Utils.getColorAttrDefaultColor(context, android.R.attr.colorAccent);
                } else if (useQSAccentTint == 2){
                      if (rgbTint || qsTileStyle == 27 || isgradient)
                             return randomColor(context);
                      else if (qsTileStyle == 7 || qsTileStyle == 9 || qsTileStyle == 10 ||
                               qsTileStyle == 12 || qsTileStyle == 13 || qsTileStyle == 16 || qsTileStyle == 17)
                             return Utils.getColorAttrDefaultColor(context, android.R.attr.textColorPrimary);
                      else 
                             return Utils.getColorAttrDefaultColor(context, android.R.attr.colorPrimary);
                }  else if (useQSAccentTint == 3) {
                            if (rgbTint || qsTileStyle == 27 || isgradient)
                                return ColorUtils.genRandomAccentColor(isThemeDark(context));
                            else if (qsIconPrimary || (qsTileStyle == 7 || qsTileStyle == 9 || qsTileStyle == 10 ||
                                qsTileStyle == 12 || qsTileStyle == 13 || qsTileStyle == 16 || qsTileStyle == 17 || qsTileStyle == 27))
                                return Utils.getColorAttrDefaultColor(context, android.R.attr.textColorPrimary);
                            else
                                return Utils.getColorAttrDefaultColor(context, android.R.attr.colorPrimary);
                } else if (useQSAccentTint == 0) {
                            if (qsIconPrimary || (qsTileStyle == 7 || qsTileStyle == 9 || qsTileStyle == 10 ||
                                qsTileStyle == 12 || qsTileStyle == 13 || qsTileStyle == 16 || qsTileStyle == 17 || qsTileStyle == 27))
                                return Utils.getColorAttrDefaultColor(context, android.R.attr.textColorPrimary);
                            else
                                return Utils.getColorAttrDefaultColor(context, android.R.attr.colorPrimary);
                }   else if (useQSAccentTint == 4) {
                            if (rgbTint || qsTileStyle == 27 || isgradient) {
                                if (isThemeDark(context))
                                    return ColorUtils.genRandomAccentColor(isThemeDark(context));
                                 else 
                                    return randomColor(context);
                            }
                            else if (qsIconPrimary || (qsTileStyle == 7 || qsTileStyle == 9 || qsTileStyle == 10 ||
                                qsTileStyle == 12 || qsTileStyle == 13 || qsTileStyle == 16 || qsTileStyle == 17 || qsTileStyle == 27))
                                return Utils.getColorAttrDefaultColor(context, android.R.attr.textColorPrimary);
                            else
                                return Utils.getColorAttrDefaultColor(context, android.R.attr.colorPrimary);
                }   else if (useQSAccentTint == 5) {
                            if (rgbTint || qsTileStyle == 27 || isgradient) {
                                if (isThemeDark(context))
                                    return randomColor(context);
                                 else
                                    return ColorUtils.genRandomAccentColor(isThemeDark(context));
                            }
                            else if (qsIconPrimary || (qsTileStyle == 7 || qsTileStyle == 9 || qsTileStyle == 10 ||
                                qsTileStyle == 12 || qsTileStyle == 13 || qsTileStyle == 16 || qsTileStyle == 17 || qsTileStyle == 27))
                                return Utils.getColorAttrDefaultColor(context, android.R.attr.textColorPrimary);
                            else
                                return Utils.getColorAttrDefaultColor(context, android.R.attr.colorPrimary);
                } 
            default:
                Log.e("QSTile", "Invalid state " + state);
                return 0;
        }
    }

    public static int randomColor(Context context) {
        Random r = new Random();
        float hsl[] = new float[3];
        hsl[0] = r.nextInt(360);
        hsl[1] = r.nextFloat();
        hsl[2] = (isThemeDark(context) ? 0.575f : 0.3f) + (r.nextFloat() * 0.125f);
        return com.android.internal.graphics.ColorUtils.HSLToColor(hsl);
    }

    private static Boolean isThemeDark(Context context) {
        switch (context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK) {
            case Configuration.UI_MODE_NIGHT_YES:
              return true;
            case Configuration.UI_MODE_NIGHT_NO:
              return false;
            default:
              return false;
        }
     }

    protected final class H extends Handler {
        private static final int ADD_CALLBACK = 1;
        private static final int CLICK = 2;
        private static final int SECONDARY_CLICK = 3;
        private static final int LONG_CLICK = 4;
        private static final int REFRESH_STATE = 5;
        private static final int SHOW_DETAIL = 6;
        private static final int USER_SWITCH = 7;
        private static final int TOGGLE_STATE_CHANGED = 8;
        private static final int SCAN_STATE_CHANGED = 9;
        private static final int DESTROY = 10;
        private static final int REMOVE_CALLBACKS = 11;
        private static final int REMOVE_CALLBACK = 12;
        private static final int SET_LISTENING = 13;
        private static final int STALE = 14;

        @VisibleForTesting
        protected H(Looper looper) {
            super(looper);
        }

        @Override
        public void handleMessage(Message msg) {
            String name = null;
            try {
                if (msg.what == ADD_CALLBACK) {
                    name = "handleAddCallback";
                    handleAddCallback((QSTile.Callback) msg.obj);
                } else if (msg.what == REMOVE_CALLBACKS) {
                    name = "handleRemoveCallbacks";
                    handleRemoveCallbacks();
                } else if (msg.what == REMOVE_CALLBACK) {
                    name = "handleRemoveCallback";
                    handleRemoveCallback((QSTile.Callback) msg.obj);
                } else if (msg.what == CLICK) {
                    name = "handleClick";
                    if (mState.disabledByPolicy) {
                        Intent intent = RestrictedLockUtils.getShowAdminSupportDetailsIntent(
                                mContext, mEnforcedAdmin);
                        Dependency.get(ActivityStarter.class).postStartActivityDismissingKeyguard(
                                intent, 0);
                    } else {
                        handleClick();
                    }
                } else if (msg.what == SECONDARY_CLICK) {
                    name = "handleSecondaryClick";
                    handleSecondaryClick();
                } else if (msg.what == LONG_CLICK) {
                    name = "handleLongClick";
                    handleLongClick();
                } else if (msg.what == REFRESH_STATE) {
                    name = "handleRefreshState";
                    handleRefreshState(msg.obj);
                } else if (msg.what == SHOW_DETAIL) {
                    name = "handleShowDetail";
                    handleShowDetail(msg.arg1 != 0);
                } else if (msg.what == USER_SWITCH) {
                    name = "handleUserSwitch";
                    handleUserSwitch(msg.arg1);
                } else if (msg.what == TOGGLE_STATE_CHANGED) {
                    name = "handleToggleStateChanged";
                    handleToggleStateChanged(msg.arg1 != 0);
                } else if (msg.what == SCAN_STATE_CHANGED) {
                    name = "handleScanStateChanged";
                    handleScanStateChanged(msg.arg1 != 0);
                } else if (msg.what == DESTROY) {
                    name = "handleDestroy";
                    handleDestroy();
                } else if (msg.what == SET_LISTENING) {
                    name = "handleSetListeningInternal";
                    handleSetListeningInternal(msg.obj, msg.arg1 != 0);
                } else if (msg.what == STALE) {
                    name = "handleStale";
                    handleStale();
                } else {
                    throw new IllegalArgumentException("Unknown msg: " + msg.what);
                }
            } catch (Throwable t) {
                final String error = "Error in " + name;
                Log.w(TAG, error, t);
                mHost.warn(error, t);
            }
        }
    }

    public static class DrawableIcon extends Icon {
        protected final Drawable mDrawable;
        protected final Drawable mInvisibleDrawable;

        public DrawableIcon(Drawable drawable) {
            mDrawable = drawable;
            mInvisibleDrawable = drawable.getConstantState().newDrawable();
        }

        @Override
        public Drawable getDrawable(Context context) {
            return mDrawable;
        }

        @Override
        public Drawable getInvisibleDrawable(Context context) {
            return mInvisibleDrawable;
        }
    }

    public static class DrawableIconWithRes extends DrawableIcon {
        private final int mId;

        public DrawableIconWithRes(Drawable drawable, int id) {
            super(drawable);
            mId = id;
        }

        @Override
        public boolean equals(Object o) {
            return o instanceof DrawableIconWithRes && ((DrawableIconWithRes) o).mId == mId;
        }
    }

    public static class ResourceIcon extends Icon {
        private static final SparseArray<Icon> ICONS = new SparseArray<Icon>();

        protected final int mResId;

        private ResourceIcon(int resId) {
            mResId = resId;
        }

        public static synchronized Icon get(int resId) {
            Icon icon = ICONS.get(resId);
            if (icon == null) {
                icon = new ResourceIcon(resId);
                ICONS.put(resId, icon);
            }
            return icon;
        }

        @Override
        public Drawable getDrawable(Context context) {
            return context.getDrawable(mResId);
        }

        @Override
        public Drawable getInvisibleDrawable(Context context) {
            return context.getDrawable(mResId);
        }

        @Override
        public boolean equals(Object o) {
            return o instanceof ResourceIcon && ((ResourceIcon) o).mResId == mResId;
        }

        @Override
        public String toString() {
            return String.format("ResourceIcon[resId=0x%08x]", mResId);
        }
    }

    protected static class AnimationIcon extends ResourceIcon {
        private final int mAnimatedResId;

        public AnimationIcon(int resId, int staticResId) {
            super(staticResId);
            mAnimatedResId = resId;
        }

        @Override
        public Drawable getDrawable(Context context) {
            // workaround: get a clean state for every new AVD
            return context.getDrawable(mAnimatedResId).getConstantState().newDrawable();
        }
    }

    @Override
    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
        pw.println(this.getClass().getSimpleName() + ":");
        pw.print("    "); pw.println(getState().toString());
    }
}
