/*
 * Copyright (C) 2015 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 android.graphics.drawable;

import static android.content.Context.CONTEXT_INCLUDE_CODE;
import static android.content.Context.CONTEXT_RESTRICTED;

import static com.android.graphics.flags.Flags.iconLoadDrawableReturnNullWhenUriDecodeFails;

import android.annotation.ColorInt;
import android.annotation.DrawableRes;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.IUriGrantsManager;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.ContentProvider;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.BlendMode;
import android.graphics.PorterDuff;
import android.graphics.RecordingCanvas;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Handler;
import android.os.Message;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.Process;
import android.os.RemoteException;
import android.os.UserHandle;
import android.text.TextUtils;
import android.util.Log;

import androidx.annotation.RequiresPermission;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Arrays;
import java.util.Objects;

/**
 * An umbrella container for several serializable graphics representations, including Bitmaps,
 * compressed bitmap images (e.g. JPG or PNG), and drawable resources (including vectors).
 *
 * <a href="https://developer.android.com/training/displaying-bitmaps/index.html">Much ink</a>
 * has been spilled on the best way to load images, and many clients may have different needs when
 * it comes to threading and fetching. This class is therefore focused on encapsulation rather than
 * behavior.
 */

public final class Icon implements Parcelable {
    private static final String TAG = "Icon";
    private static final boolean DEBUG = false;

    /**
     * An icon that was created using {@link Icon#createWithBitmap(Bitmap)}.
     * @see #getType
     */
    public static final int TYPE_BITMAP   = 1;
    /**
     * An icon that was created using {@link Icon#createWithResource}.
     * @see #getType
     */
    public static final int TYPE_RESOURCE = 2;
    /**
     * An icon that was created using {@link Icon#createWithData(byte[], int, int)}.
     * @see #getType
     */
    public static final int TYPE_DATA     = 3;
    /**
     * An icon that was created using {@link Icon#createWithContentUri}
     * or {@link Icon#createWithFilePath(String)}.
     * @see #getType
     */
    public static final int TYPE_URI      = 4;
    /**
     * An icon that was created using {@link Icon#createWithAdaptiveBitmap}.
     * @see #getType
     */
    public static final int TYPE_ADAPTIVE_BITMAP = 5;
    /**
     * An icon that was created using {@link Icon#createWithAdaptiveBitmapContentUri}.
     * @see #getType
     */
    public static final int TYPE_URI_ADAPTIVE_BITMAP = 6;

    /**
     * @hide
     */
    @IntDef({TYPE_BITMAP, TYPE_RESOURCE, TYPE_DATA, TYPE_URI, TYPE_ADAPTIVE_BITMAP,
            TYPE_URI_ADAPTIVE_BITMAP})
    @Retention(RetentionPolicy.SOURCE)
    public @interface IconType {
    }

    private static final int VERSION_STREAM_SERIALIZER = 1;

    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
    private final int mType;

    private ColorStateList mTintList;
    static final BlendMode DEFAULT_BLEND_MODE = Drawable.DEFAULT_BLEND_MODE; // SRC_IN
    private BlendMode mBlendMode = Drawable.DEFAULT_BLEND_MODE;

    // To avoid adding unnecessary overhead, we have a few basic objects that get repurposed
    // based on the value of mType.

    // TYPE_BITMAP: Bitmap
    // TYPE_ADAPTIVE_BITMAP: Bitmap
    // TYPE_RESOURCE: Resources
    // TYPE_DATA: DataBytes
    private Object          mObj1;
    private boolean mCachedAshmem = false;

    // TYPE_RESOURCE: package name
    // TYPE_URI: uri string
    // TYPE_URI_ADAPTIVE_BITMAP: uri string
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
    private String          mString1;

    // TYPE_RESOURCE: resId
    // TYPE_DATA: data length
    private int             mInt1;

    // TYPE_DATA: data offset
    private int             mInt2;

    // TYPE_RESOURCE: use the monochrome drawable from an AdaptiveIconDrawable
    private boolean mUseMonochrome = false;

    // TYPE_RESOURCE: wrap the monochrome drawable in an InsetDrawable with the specified inset
    private float mInsetScale = 0.0f;

    /**
     * Gets the type of the icon provided.
     * <p>
     * Note that new types may be added later, so callers should guard against other
     * types being returned.
     */
    @IconType
    public int getType() {
        return mType;
    }

    /**
     * @return The {@link android.graphics.Bitmap} held by this {@link #TYPE_BITMAP} or
     * {@link #TYPE_ADAPTIVE_BITMAP} Icon.
     *
     * Note that this will always return an immutable Bitmap.
     * @hide
     */
    @UnsupportedAppUsage
    public Bitmap getBitmap() {
        if (mType != TYPE_BITMAP && mType != TYPE_ADAPTIVE_BITMAP) {
            throw new IllegalStateException("called getBitmap() on " + this);
        }
        return (Bitmap) mObj1;
    }

    /**
     * Sets the Icon's contents to a particular Bitmap. Note that this may make a copy of the Bitmap
     * if the supplied Bitmap is mutable. In that case, the value returned by getBitmap() may not
     * equal the Bitmap passed to setBitmap().
     *
     * @hide
     */
    private void setBitmap(Bitmap b) {
        if (b.isMutable()) {
            mObj1 = b.copy(b.getConfig(), false);
        } else {
            mObj1 = b;
        }
        mCachedAshmem = false;
    }

    /**
     * @return The length of the compressed bitmap byte array held by this {@link #TYPE_DATA} Icon.
     * @hide
     */
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    public int getDataLength() {
        if (mType != TYPE_DATA) {
            throw new IllegalStateException("called getDataLength() on " + this);
        }
        synchronized (this) {
            return mInt1;
        }
    }

    /**
     * @return The offset into the byte array held by this {@link #TYPE_DATA} Icon at which
     * valid compressed bitmap data is found.
     * @hide
     */
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
    public int getDataOffset() {
        if (mType != TYPE_DATA) {
            throw new IllegalStateException("called getDataOffset() on " + this);
        }
        synchronized (this) {
            return mInt2;
        }
    }

    /**
     * @return The byte array held by this {@link #TYPE_DATA} Icon ctonaining compressed
     * bitmap data.
     * @hide
     */
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
    public byte[] getDataBytes() {
        if (mType != TYPE_DATA) {
            throw new IllegalStateException("called getDataBytes() on " + this);
        }
        synchronized (this) {
            return (byte[]) mObj1;
        }
    }

    /**
     * @return The {@link android.content.res.Resources} for this {@link #TYPE_RESOURCE} Icon.
     * @hide
     */
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
    public Resources getResources() {
        if (mType != TYPE_RESOURCE) {
            throw new IllegalStateException("called getResources() on " + this);
        }
        return (Resources) mObj1;
    }

    /**
     * Gets the package used to create this icon.
     * <p>
     * Only valid for icons of type {@link #TYPE_RESOURCE}.
     * Note: This package may not be available if referenced in the future, and it is
     * up to the caller to ensure safety if this package is re-used and/or persisted.
     */
    @NonNull
    public String getResPackage() {
        if (mType != TYPE_RESOURCE) {
            throw new IllegalStateException("called getResPackage() on " + this);
        }
        return mString1;
    }

    /**
     * Gets the resource used to create this icon.
     * <p>
     * Only valid for icons of type {@link #TYPE_RESOURCE}.
     * Note: This resource may not be available if the application changes at all, and it is
     * up to the caller to ensure safety if this resource is re-used and/or persisted.
     */
    @DrawableRes
    public int getResId() {
        if (mType != TYPE_RESOURCE) {
            throw new IllegalStateException("called getResId() on " + this);
        }
        return mInt1;
    }

    /**
     * @return The URI (as a String) for this {@link #TYPE_URI} or {@link #TYPE_URI_ADAPTIVE_BITMAP}
     * Icon.
     * @hide
     */
    public String getUriString() {
        if (mType != TYPE_URI && mType != TYPE_URI_ADAPTIVE_BITMAP) {
            throw new IllegalStateException("called getUriString() on " + this);
        }
        return mString1;
    }

    /**
     * Gets the uri used to create this icon.
     * <p>
     * Only valid for icons of type {@link #TYPE_URI} and {@link #TYPE_URI_ADAPTIVE_BITMAP}.
     * Note: This uri may not be available in the future, and it is
     * up to the caller to ensure safety if this uri is re-used and/or persisted.
     */
    @NonNull
    public Uri getUri() {
        return Uri.parse(getUriString());
    }

    private static final String typeToString(int x) {
        switch (x) {
            case TYPE_BITMAP: return "BITMAP";
            case TYPE_ADAPTIVE_BITMAP: return "BITMAP_MASKABLE";
            case TYPE_DATA: return "DATA";
            case TYPE_RESOURCE: return "RESOURCE";
            case TYPE_URI: return "URI";
            case TYPE_URI_ADAPTIVE_BITMAP: return "URI_MASKABLE";
            default: return "UNKNOWN";
        }
    }

    /**
     * Invokes {@link #loadDrawable(Context)} on the given {@link android.os.Handler Handler}
     * and then sends <code>andThen</code> to the same Handler when finished.
     *
     * @param context {@link android.content.Context Context} in which to load the drawable; see
     *                {@link #loadDrawable(Context)}
     * @param andThen {@link android.os.Message} to send to its target once the drawable
     *                is available. The {@link android.os.Message#obj obj}
     *                property is populated with the Drawable.
     */
    public void loadDrawableAsync(@NonNull Context context, @NonNull Message andThen) {
        if (andThen.getTarget() == null) {
            throw new IllegalArgumentException("callback message must have a target handler");
        }
        new LoadDrawableTask(context, andThen).runAsync();
    }

    /**
     * Invokes {@link #loadDrawable(Context)} on a background thread and notifies the <code>
     * {@link OnDrawableLoadedListener#onDrawableLoaded listener} </code> on the {@code handler}
     * when finished.
     *
     * @param context {@link Context Context} in which to load the drawable; see
     *                {@link #loadDrawable(Context)}
     * @param listener to be {@link OnDrawableLoadedListener#onDrawableLoaded notified} when
     *                 {@link #loadDrawable(Context)} finished
     * @param handler {@link Handler} on which to notify the {@code listener}
     */
    public void loadDrawableAsync(@NonNull Context context, final OnDrawableLoadedListener listener,
            Handler handler) {
        new LoadDrawableTask(context, handler, listener).runAsync();
    }

    /**
     * Returns a Drawable that can be used to draw the image inside this Icon, constructing it
     * if necessary. Depending on the type of image, this may not be something you want to do on
     * the UI thread, so consider using
     * {@link #loadDrawableAsync(Context, Message) loadDrawableAsync} instead.
     *
     * @param context {@link android.content.Context Context} in which to load the drawable; used
     *                to access {@link android.content.res.Resources Resources}, for example.
     * @return A fresh instance of a drawable for this image, yours to keep.
     */
    public @Nullable Drawable loadDrawable(Context context) {
        final Drawable result = loadDrawableInner(context);
        if (result != null && hasTint()) {
            result.mutate();
            result.setTintList(mTintList);
            result.setTintBlendMode(mBlendMode);
        }

        if (mUseMonochrome) {
            return crateMonochromeDrawable(result, mInsetScale);
        }

        return result;
    }

    /**
     * Gets the monochrome drawable from an {@link AdaptiveIconDrawable}.
     *
     * @param drawable An {@link AdaptiveIconDrawable}
     * @return Adjusted (wrapped in {@link InsetDrawable}) monochrome drawable
     *  from an {@link AdaptiveIconDrawable}.
     * Or the original drawable if no monochrome layer exists.
     */
    private static Drawable crateMonochromeDrawable(Drawable drawable, float inset) {
        if (drawable instanceof AdaptiveIconDrawable) {
            Drawable monochromeDrawable = ((AdaptiveIconDrawable) drawable).getMonochrome();
            // wrap with negative inset => scale icon (inspired from BaseIconFactory)
            if (monochromeDrawable != null) {
                return new InsetDrawable(monochromeDrawable, inset);
            }
        }
        return drawable;
    }

    /**
     * Resizes image if size too large for Canvas to draw
     * @param bitmap Bitmap to be resized if size > {@link RecordingCanvas.MAX_BITMAP_SIZE}
     * @return resized bitmap
     */
    private Bitmap fixMaxBitmapSize(Bitmap bitmap) {
        if (bitmap != null && bitmap.getByteCount() > RecordingCanvas.MAX_BITMAP_SIZE) {
            int bytesPerPixel = bitmap.getRowBytes() / bitmap.getWidth();
            int maxNumPixels = RecordingCanvas.MAX_BITMAP_SIZE / bytesPerPixel;
            float aspRatio = (float) bitmap.getWidth() / (float) bitmap.getHeight();
            int newHeight = (int) Math.sqrt(maxNumPixels / aspRatio);
            int newWidth = (int) (newHeight * aspRatio);

            if (DEBUG) {
                Log.d(TAG,
                        "Image size too large: " + bitmap.getByteCount() + ". Resizing bitmap to: "
                                + newWidth + " " + newHeight);
            }

            return scaleDownIfNecessary(bitmap, newWidth, newHeight);
        }
        return bitmap;
    }

    /**
     * Resizes BitmapDrawable if size too large for Canvas to draw
     * @param drawable Drawable to be resized if size > {@link RecordingCanvas.MAX_BITMAP_SIZE}
     * @return resized Drawable
     */
    private Drawable fixMaxBitmapSize(Resources res, Drawable drawable) {
        if (drawable instanceof BitmapDrawable) {
            Bitmap scaledBmp = fixMaxBitmapSize(((BitmapDrawable) drawable).getBitmap());
            return new BitmapDrawable(res, scaledBmp);
        }
        return drawable;
    }

    /**
     * Do the heavy lifting of loading the drawable, but stop short of applying any tint.
     */
    private Drawable loadDrawableInner(Context context) {
        switch (mType) {
            case TYPE_BITMAP:
                return new BitmapDrawable(context.getResources(), fixMaxBitmapSize(getBitmap()));
            case TYPE_ADAPTIVE_BITMAP:
                return new AdaptiveIconDrawable(null,
                    new BitmapDrawable(context.getResources(), fixMaxBitmapSize(getBitmap())));
            case TYPE_RESOURCE:
                if (getResources() == null) {
                    // figure out where to load resources from
                    String resPackage = getResPackage();
                    if (TextUtils.isEmpty(resPackage)) {
                        // if none is specified, try the given context
                        resPackage = context.getPackageName();
                    }
                    if ("android".equals(resPackage)) {
                        mObj1 = Resources.getSystem();
                    } else {
                        final PackageManager pm = context.getPackageManager();
                        try {
                            ApplicationInfo ai = pm.getApplicationInfo(
                                    resPackage,
                                    PackageManager.MATCH_UNINSTALLED_PACKAGES
                                    | PackageManager.GET_SHARED_LIBRARY_FILES);
                            if (ai != null) {
                                mObj1 = pm.getResourcesForApplication(ai);
                            } else {
                                break;
                            }
                        } catch (PackageManager.NameNotFoundException e) {
                            Log.e(TAG, String.format("Unable to find pkg=%s for icon %s",
                                    resPackage, this), e);
                            break;
                        }
                    }
                }
                try {
                    return fixMaxBitmapSize(getResources(),
                            getResources().getDrawable(getResId(), context.getTheme()));
                } catch (RuntimeException e) {
                    Log.e(TAG, String.format("Unable to load resource 0x%08x from pkg=%s",
                                    getResId(),
                                    getResPackage()),
                            e);
                }
                break;
            case TYPE_DATA:
                return new BitmapDrawable(context.getResources(), fixMaxBitmapSize(
                        BitmapFactory.decodeByteArray(getDataBytes(), getDataOffset(),
                                getDataLength())));
            case TYPE_URI:
                InputStream is = getUriInputStream(context);
                if (is != null) {
                    final Bitmap bitmap = BitmapFactory.decodeStream(is);
                    if (bitmap == null) {
                        Log.w(TAG, "Unable to decode image from URI: " + getUriString());
                        if (iconLoadDrawableReturnNullWhenUriDecodeFails()) {
                            return null;
                        }
                    }
                    return new BitmapDrawable(context.getResources(), fixMaxBitmapSize(bitmap));
                }
                break;
            case TYPE_URI_ADAPTIVE_BITMAP:
                is = getUriInputStream(context);
                if (is != null) {
                    final Bitmap bitmap = BitmapFactory.decodeStream(is);
                    if (bitmap == null) {
                        Log.w(TAG, "Unable to decode image from URI: " + getUriString());
                        if (iconLoadDrawableReturnNullWhenUriDecodeFails()) {
                            return null;
                        }
                    }
                    return new AdaptiveIconDrawable(null, new BitmapDrawable(context.getResources(),
                            fixMaxBitmapSize(bitmap)));
                }
                break;
        }
        return null;
    }

    private @Nullable InputStream getUriInputStream(Context context) {
        final Uri uri = getUri();
        final String scheme = uri.getScheme();
        if (ContentResolver.SCHEME_CONTENT.equals(scheme)
                || ContentResolver.SCHEME_FILE.equals(scheme)) {
            try {
                return context.getContentResolver().openInputStream(uri);
            } catch (Exception e) {
                Log.w(TAG, "Unable to load image from URI: " + uri, e);
            }
        } else {
            try {
                return new FileInputStream(new File(mString1));
            } catch (FileNotFoundException e) {
                Log.w(TAG, "Unable to load image from path: " + uri, e);
            }
        }
        return null;
    }

    /**
     * Load the requested resources under the given userId, if the system allows it,
     * before actually loading the drawable.
     *
     * @hide
     */
    public Drawable loadDrawableAsUser(Context context, int userId) {
        if (mType == TYPE_RESOURCE) {
            String resPackage = getResPackage();
            if (TextUtils.isEmpty(resPackage)) {
                resPackage = context.getPackageName();
            }
            if (getResources() == null && !(getResPackage().equals("android"))) {
                // TODO(b/173307037): Move CONTEXT_INCLUDE_CODE to ContextImpl.createContextAsUser
                final Context userContext;
                if (context.getUserId() == userId) {
                    userContext = context;
                } else {
                    final boolean sameAppWithProcess =
                            UserHandle.isSameApp(context.getApplicationInfo().uid, Process.myUid());
                    final int flags = (sameAppWithProcess ? CONTEXT_INCLUDE_CODE : 0)
                            | CONTEXT_RESTRICTED;
                    userContext = context.createContextAsUser(UserHandle.of(userId), flags);
                }

                final PackageManager pm = userContext.getPackageManager();
                try {
                    // assign getResources() as the correct user
                    mObj1 = pm.getResourcesForApplication(resPackage);
                } catch (PackageManager.NameNotFoundException e) {
                    Log.e(TAG, String.format("Unable to find pkg=%s user=%d",
                                    getResPackage(),
                                    userId),
                            e);
                }
            }
        }
        return loadDrawable(context);
    }

    /**
     * Load a drawable, but in the case of URI types, it will check if the passed uid has a grant
     * to load the resource. The check will be performed using the permissions of the passed uid,
     * and not those of the caller.
     * <p>
     * This should be called for {@link Icon} objects that come from a not trusted source and may
     * contain a URI.
     *
     * After the check, if passed, {@link #loadDrawable} will be called. If failed, this will
     * return {@code null}.
     *
     * @see #loadDrawable
     *
     * @hide
     */
    @Nullable
    @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL)
    public Drawable loadDrawableCheckingUriGrant(
            Context context,
            IUriGrantsManager iugm,
            int callingUid,
            String packageName
    ) {
        if (getType() == TYPE_URI || getType() == TYPE_URI_ADAPTIVE_BITMAP) {
            try {
                iugm.checkGrantUriPermission_ignoreNonSystem(
                        callingUid,
                        packageName,
                        ContentProvider.getUriWithoutUserId(getUri()),
                        Intent.FLAG_GRANT_READ_URI_PERMISSION,
                        ContentProvider.getUserIdFromUri(getUri())
                );
            } catch (SecurityException | RemoteException e) {
                Log.e(TAG, "Failed to get URI permission for: " + getUri(), e);
                return null;
            }
        }
        return loadDrawable(context);
    }

    /** @hide */
    public static final int MIN_ASHMEM_ICON_SIZE = 128 * (1 << 10);

    /**
     * Puts the memory used by this instance into Ashmem memory, if possible.
     * @hide
     */
    public void convertToAshmem() {
        if ((mType == TYPE_BITMAP || mType == TYPE_ADAPTIVE_BITMAP) &&
            getBitmap().isMutable() &&
            getBitmap().getAllocationByteCount() >= MIN_ASHMEM_ICON_SIZE) {
            setBitmap(getBitmap().asShared());
        }
        mCachedAshmem = true;
    }

    /**
     * Writes a serialized version of an Icon to the specified stream.
     *
     * @param stream The stream on which to serialize the Icon.
     * @hide
     */
    public void writeToStream(@NonNull OutputStream stream) throws IOException {
        DataOutputStream dataStream = new DataOutputStream(stream);

        dataStream.writeInt(VERSION_STREAM_SERIALIZER);
        dataStream.writeByte(mType);

        switch (mType) {
            case TYPE_BITMAP:
            case TYPE_ADAPTIVE_BITMAP:
                getBitmap().compress(Bitmap.CompressFormat.PNG, 100, dataStream);
                break;
            case TYPE_DATA:
                dataStream.writeInt(getDataLength());
                dataStream.write(getDataBytes(), getDataOffset(), getDataLength());
                break;
            case TYPE_RESOURCE:
                dataStream.writeUTF(getResPackage());
                dataStream.writeInt(getResId());
                break;
            case TYPE_URI:
            case TYPE_URI_ADAPTIVE_BITMAP:
                dataStream.writeUTF(getUriString());
                break;
        }
    }

    private Icon(int mType) {
        this.mType = mType;
    }

    /**
     * Create an Icon from the specified stream.
     *
     * @param stream The input stream from which to reconstruct the Icon.
     * @hide
     */
    public static @Nullable Icon createFromStream(@NonNull InputStream stream) throws IOException {
        DataInputStream inputStream = new DataInputStream(stream);

        final int version = inputStream.readInt();
        if (version >= VERSION_STREAM_SERIALIZER) {
            final int type = inputStream.readByte();
            switch (type) {
                case TYPE_BITMAP:
                    return createWithBitmap(BitmapFactory.decodeStream(inputStream));
                case TYPE_ADAPTIVE_BITMAP:
                    return createWithAdaptiveBitmap(BitmapFactory.decodeStream(inputStream));
                case TYPE_DATA:
                    final int length = inputStream.readInt();
                    final byte[] data = new byte[length];
                    inputStream.read(data, 0 /* offset */, length);
                    return createWithData(data, 0 /* offset */, length);
                case TYPE_RESOURCE:
                    final String packageName = inputStream.readUTF();
                    final int resId = inputStream.readInt();
                    return createWithResource(packageName, resId);
                case TYPE_URI:
                    final String uriOrPath = inputStream.readUTF();
                    return createWithContentUri(uriOrPath);
                case TYPE_URI_ADAPTIVE_BITMAP:
                    final String uri = inputStream.readUTF();
                    return createWithAdaptiveBitmapContentUri(uri);
            }
        }
        return null;
    }

    /**
     * Compares if this icon is constructed from the same resources as another icon.
     * Note that this is an inexpensive operation and doesn't do deep Bitmap equality comparisons.
     *
     * @param otherIcon the other icon
     * @return whether this icon is the same as the another one
     * @hide
     */
    public boolean sameAs(@NonNull Icon otherIcon) {
        if (otherIcon == this) {
            return true;
        }
        if (mType != otherIcon.getType()) {
            return false;
        }
        switch (mType) {
            case TYPE_BITMAP:
            case TYPE_ADAPTIVE_BITMAP:
                return getBitmap() == otherIcon.getBitmap();
            case TYPE_DATA:
                return getDataLength() == otherIcon.getDataLength()
                        && getDataOffset() == otherIcon.getDataOffset()
                        && Arrays.equals(getDataBytes(), otherIcon.getDataBytes());
            case TYPE_RESOURCE:
                return getResId() == otherIcon.getResId()
                        && Objects.equals(getResPackage(), otherIcon.getResPackage())
                        && mUseMonochrome == otherIcon.mUseMonochrome
                        && mInsetScale == otherIcon.mInsetScale;
            case TYPE_URI:
            case TYPE_URI_ADAPTIVE_BITMAP:
                return Objects.equals(getUriString(), otherIcon.getUriString());
        }
        return false;
    }

    /**
     * Create an Icon pointing to a drawable resource.
     * @param context The context for the application whose resources should be used to resolve the
     *                given resource ID.
     * @param resId ID of the drawable resource
     */
    public static @NonNull Icon createWithResource(Context context, @DrawableRes int resId) {
        if (context == null) {
            throw new IllegalArgumentException("Context must not be null.");
        }
        final Icon rep = new Icon(TYPE_RESOURCE);
        rep.mInt1 = resId;
        rep.mString1 = context.getPackageName();
        return rep;
    }

    /**
     * Version of createWithResource that takes Resources. Do not use.
     * @hide
     */
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    public static @NonNull Icon createWithResource(Resources res, @DrawableRes int resId) {
        if (res == null) {
            throw new IllegalArgumentException("Resource must not be null.");
        }
        final Icon rep = new Icon(TYPE_RESOURCE);
        rep.mInt1 = resId;
        rep.mString1 = res.getResourcePackageName(resId);
        return rep;
    }

    /**
     * Create an Icon pointing to a drawable resource.
     * @param resPackage Name of the package containing the resource in question
     * @param resId ID of the drawable resource
     */
    public static @NonNull Icon createWithResource(String resPackage, @DrawableRes int resId) {
        if (resPackage == null) {
            throw new IllegalArgumentException("Resource package name must not be null.");
        }
        final Icon rep = new Icon(TYPE_RESOURCE);
        rep.mInt1 = resId;
        rep.mString1 = resPackage;
        return rep;
    }

    /**
     * Create an Icon pointing to a drawable resource.
     * @param resPackage Name of the package containing the resource in question
     * @param resId ID of the drawable resource
     * @param useMonochrome if this icon should use the monochrome res from the adaptive drawable
     * @hide
     */
    public static @NonNull Icon createWithResourceAdaptiveDrawable(@NonNull String resPackage,
            @DrawableRes int resId, boolean useMonochrome, float inset) {
        if (resPackage == null) {
            throw new IllegalArgumentException("Resource package name must not be null.");
        }
        final Icon rep = new Icon(TYPE_RESOURCE);
        rep.mInt1 = resId;
        rep.mUseMonochrome = useMonochrome;
        rep.mInsetScale = inset;
        rep.mString1 = resPackage;
        return rep;
    }

    /**
     * Create an Icon pointing to a bitmap in memory.
     * @param bits A valid {@link android.graphics.Bitmap} object
     */
    public static @NonNull Icon createWithBitmap(Bitmap bits) {
        if (bits == null) {
            throw new IllegalArgumentException("Bitmap must not be null.");
        }
        final Icon rep = new Icon(TYPE_BITMAP);
        rep.setBitmap(bits);
        return rep;
    }

    /**
     * Create an Icon pointing to a bitmap in memory that follows the icon design guideline defined
     * by {@link AdaptiveIconDrawable}.
     * @param bits A valid {@link android.graphics.Bitmap} object
     */
    public static @NonNull Icon createWithAdaptiveBitmap(Bitmap bits) {
        if (bits == null) {
            throw new IllegalArgumentException("Bitmap must not be null.");
        }
        final Icon rep = new Icon(TYPE_ADAPTIVE_BITMAP);
        rep.setBitmap(bits);
        return rep;
    }

    /**
     * Create an Icon pointing to a compressed bitmap stored in a byte array.
     * @param data Byte array storing compressed bitmap data of a type that
     *             {@link android.graphics.BitmapFactory}
     *             can decode (see {@link android.graphics.Bitmap.CompressFormat}).
     * @param offset Offset into <code>data</code> at which the bitmap data starts
     * @param length Length of the bitmap data
     */
    public static @NonNull Icon createWithData(byte[] data, int offset, int length) {
        if (data == null) {
            throw new IllegalArgumentException("Data must not be null.");
        }
        final Icon rep = new Icon(TYPE_DATA);
        rep.mObj1 = data;
        rep.mInt1 = length;
        rep.mInt2 = offset;
        return rep;
    }

    /**
     * Create an Icon pointing to an image file specified by URI.
     *
     * @param uri A uri referring to local content:// or file:// image data.
     */
    public static @NonNull Icon createWithContentUri(String uri) {
        if (uri == null) {
            throw new IllegalArgumentException("Uri must not be null.");
        }
        final Icon rep = new Icon(TYPE_URI);
        rep.mString1 = uri;
        return rep;
    }

    /**
     * Create an Icon pointing to an image file specified by URI.
     *
     * @param uri A uri referring to local content:// or file:// image data.
     */
    public static @NonNull Icon createWithContentUri(Uri uri) {
        if (uri == null) {
            throw new IllegalArgumentException("Uri must not be null.");
        }
        return createWithContentUri(uri.toString());
    }

    /**
     * Create an Icon pointing to an image file specified by URI. Image file should follow the icon
     * design guideline defined by {@link AdaptiveIconDrawable}.
     *
     * @param uri A uri referring to local content:// or file:// image data.
     */
    public static @NonNull Icon createWithAdaptiveBitmapContentUri(@NonNull String uri) {
        if (uri == null) {
            throw new IllegalArgumentException("Uri must not be null.");
        }
        final Icon rep = new Icon(TYPE_URI_ADAPTIVE_BITMAP);
        rep.mString1 = uri;
        return rep;
    }

    /**
     * Create an Icon pointing to an image file specified by URI. Image file should follow the icon
     * design guideline defined by {@link AdaptiveIconDrawable}.
     *
     * @param uri A uri referring to local content:// or file:// image data.
     */
    @NonNull
    public static Icon createWithAdaptiveBitmapContentUri(@NonNull Uri uri) {
        if (uri == null) {
            throw new IllegalArgumentException("Uri must not be null.");
        }
        return createWithAdaptiveBitmapContentUri(uri.toString());
    }

    /**
     * Store a color to use whenever this Icon is drawn.
     *
     * @param tint a color, as in {@link Drawable#setTint(int)}
     * @return this same object, for use in chained construction
     */
    public @NonNull Icon setTint(@ColorInt int tint) {
        return setTintList(ColorStateList.valueOf(tint));
    }

    /**
     * Store a color to use whenever this Icon is drawn.
     *
     * @param tintList as in {@link Drawable#setTintList(ColorStateList)}, null to remove tint
     * @return this same object, for use in chained construction
     */
    public @NonNull Icon setTintList(ColorStateList tintList) {
        mTintList = tintList;
        return this;
    }

    /** @hide */
    public @Nullable ColorStateList getTintList() {
        return mTintList;
    }

    /**
     * Store a blending mode to use whenever this Icon is drawn.
     *
     * @param mode a blending mode, as in {@link Drawable#setTintMode(PorterDuff.Mode)}, may be null
     * @return this same object, for use in chained construction
     */
    public @NonNull Icon setTintMode(@NonNull PorterDuff.Mode mode) {
        mBlendMode = BlendMode.fromValue(mode.nativeInt);
        return this;
    }

    /**
     * Store a blending mode to use whenever this Icon is drawn.
     *
     * @param mode a blending mode, as in {@link Drawable#setTintMode(PorterDuff.Mode)}, may be null
     * @return this same object, for use in chained construction
     */
    public @NonNull Icon setTintBlendMode(@NonNull BlendMode mode) {
        mBlendMode = mode;
        return this;
    }

    /** @hide */
    public @NonNull BlendMode getTintBlendMode() {
        return mBlendMode;
    }

    /** @hide */
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    public boolean hasTint() {
        return (mTintList != null) || (mBlendMode != DEFAULT_BLEND_MODE);
    }

    /**
     * Create an Icon pointing to an image file specified by path.
     *
     * @param path A path to a file that contains compressed bitmap data of
     *           a type that {@link android.graphics.BitmapFactory} can decode.
     */
    public static @NonNull Icon createWithFilePath(String path) {
        if (path == null) {
            throw new IllegalArgumentException("Path must not be null.");
        }
        final Icon rep = new Icon(TYPE_URI);
        rep.mString1 = path;
        return rep;
    }

    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder("Icon(typ=").append(typeToString(mType));
        switch (mType) {
            case TYPE_BITMAP:
            case TYPE_ADAPTIVE_BITMAP:
                sb.append(" size=")
                        .append(getBitmap().getWidth())
                        .append("x")
                        .append(getBitmap().getHeight());
                break;
            case TYPE_RESOURCE:
                sb.append(" pkg=")
                        .append(getResPackage())
                        .append(" id=")
                        .append(String.format("0x%08x", getResId()));
                break;
            case TYPE_DATA:
                sb.append(" len=").append(getDataLength());
                if (getDataOffset() != 0) {
                    sb.append(" off=").append(getDataOffset());
                }
                break;
            case TYPE_URI:
            case TYPE_URI_ADAPTIVE_BITMAP:
                sb.append(" uri=").append(getUriString());
                break;
        }
        if (mTintList != null) {
            sb.append(" tint=");
            String sep = "";
            for (int c : mTintList.getColors()) {
                sb.append(String.format("%s0x%08x", sep, c));
                sep = "|";
            }
        }
        if (mBlendMode != DEFAULT_BLEND_MODE) sb.append(" mode=").append(mBlendMode);
        sb.append(")");
        return sb.toString();
    }

    /**
     * Parcelable interface
     */
    public int describeContents() {
        return (mType == TYPE_BITMAP || mType == TYPE_ADAPTIVE_BITMAP || mType == TYPE_DATA)
                ? Parcelable.CONTENTS_FILE_DESCRIPTOR : 0;
    }

    // ===== Parcelable interface ======

    private Icon(Parcel in) {
        this(in.readInt());
        switch (mType) {
            case TYPE_BITMAP:
            case TYPE_ADAPTIVE_BITMAP:
                final Bitmap bits = Bitmap.CREATOR.createFromParcel(in);
                mObj1 = bits;
                break;
            case TYPE_RESOURCE:
                final String pkg = in.readString();
                final int resId = in.readInt();
                mString1 = pkg;
                mInt1 = resId;
                mUseMonochrome = in.readBoolean();
                mInsetScale = in.readFloat();
                break;
            case TYPE_DATA:
                final int len = in.readInt();
                final byte[] a = in.readBlob();
                if (len != a.length) {
                    throw new RuntimeException("internal unparceling error: blob length ("
                            + a.length + ") != expected length (" + len + ")");
                }
                mInt1 = len;
                mObj1 = a;
                break;
            case TYPE_URI:
            case TYPE_URI_ADAPTIVE_BITMAP:
                final String uri = in.readString();
                mString1 = uri;
                break;
            default:
                throw new RuntimeException("invalid "
                        + this.getClass().getSimpleName() + " type in parcel: " + mType);
        }
        if (in.readInt() == 1) {
            mTintList = ColorStateList.CREATOR.createFromParcel(in);
        }
        mBlendMode = BlendMode.fromValue(in.readInt());
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeInt(mType);
        switch (mType) {
            case TYPE_BITMAP:
            case TYPE_ADAPTIVE_BITMAP:
                if (!mCachedAshmem) {
                    mObj1 = ((Bitmap) mObj1).asShared();
                    mCachedAshmem = true;
                }
                getBitmap().writeToParcel(dest, flags);
                break;
            case TYPE_RESOURCE:
                dest.writeString(getResPackage());
                dest.writeInt(getResId());
                dest.writeBoolean(mUseMonochrome);
                dest.writeFloat(mInsetScale);
                break;
            case TYPE_DATA:
                dest.writeInt(getDataLength());
                dest.writeBlob(getDataBytes(), getDataOffset(), getDataLength());
                break;
            case TYPE_URI:
            case TYPE_URI_ADAPTIVE_BITMAP:
                dest.writeString(getUriString());
                break;
        }
        if (mTintList == null) {
            dest.writeInt(0);
        } else {
            dest.writeInt(1);
            mTintList.writeToParcel(dest, flags);
        }
        dest.writeInt(BlendMode.toValue(mBlendMode));
    }

    public static final @android.annotation.NonNull Parcelable.Creator<Icon> CREATOR
            = new Parcelable.Creator<Icon>() {
        public Icon createFromParcel(Parcel in) {
            return new Icon(in);
        }

        public Icon[] newArray(int size) {
            return new Icon[size];
        }
    };

    /**
     * Scale down a bitmap to a given max width and max height. The scaling will be done in a uniform way
     * @param bitmap the bitmap to scale down
     * @param maxWidth the maximum width allowed
     * @param maxHeight the maximum height allowed
     *
     * @return the scaled bitmap if necessary or the original bitmap if no scaling was needed
     * @hide
     */
    public static Bitmap scaleDownIfNecessary(Bitmap bitmap, int maxWidth, int maxHeight) {
        int bitmapWidth = bitmap.getWidth();
        int bitmapHeight = bitmap.getHeight();
        if (bitmapWidth > maxWidth || bitmapHeight > maxHeight) {
            float scale = Math.min((float) maxWidth / bitmapWidth,
                    (float) maxHeight / bitmapHeight);
            bitmap = Bitmap.createScaledBitmap(bitmap,
                    Math.max(1, (int) (scale * bitmapWidth)),
                    Math.max(1, (int) (scale * bitmapHeight)),
                    true /* filter */);
        }
        return bitmap;
    }

    /**
     * Scale down this icon to a given max width and max height.
     * The scaling will be done in a uniform way and currently only bitmaps are supported.
     * @param maxWidth the maximum width allowed
     * @param maxHeight the maximum height allowed
     *
     * @hide
     */
    public void scaleDownIfNecessary(int maxWidth, int maxHeight) {
        if (mType != TYPE_BITMAP && mType != TYPE_ADAPTIVE_BITMAP) {
            return;
        }
        Bitmap bitmap = getBitmap();
        setBitmap(scaleDownIfNecessary(bitmap, maxWidth, maxHeight));
    }

    /**
     * Implement this interface to receive a callback when
     * {@link #loadDrawableAsync(Context, OnDrawableLoadedListener, Handler) loadDrawableAsync}
     * is finished and your Drawable is ready.
     */
    public interface OnDrawableLoadedListener {
        void onDrawableLoaded(Drawable d);
    }

    /**
     * Wrapper around loadDrawable that does its work on a pooled thread and then
     * fires back the given (targeted) Message.
     */
    private class LoadDrawableTask implements Runnable {
        final Context mContext;
        final Message mMessage;

        public LoadDrawableTask(Context context, final Handler handler,
                final OnDrawableLoadedListener listener) {
            mContext = context;
            mMessage = Message.obtain(handler, new Runnable() {
                    @Override
                    public void run() {
                        listener.onDrawableLoaded((Drawable) mMessage.obj);
                    }
                });
        }

        public LoadDrawableTask(Context context, Message message) {
            mContext = context;
            mMessage = message;
        }

        @Override
        public void run() {
            mMessage.obj = loadDrawable(mContext);
            mMessage.sendToTarget();
        }

        public void runAsync() {
            AsyncTask.THREAD_POOL_EXECUTOR.execute(this);
        }
    }
}
