/*
 * 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.hardware.radio;

import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemApi;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.ArrayMap;
import android.util.Log;
import android.util.SparseArray;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Set;

/**
 * Contains meta data about a radio program such as station name, song title, artist etc...
 * @hide
 */
@SystemApi
public final class RadioMetadata implements Parcelable {
    private static final String TAG = "BroadcastRadio.metadata";

    /**
     * The RDS Program Information.
     */
    public static final String METADATA_KEY_RDS_PI = "android.hardware.radio.metadata.RDS_PI";

    /**
     * The RDS Program Service.
     */
    public static final String METADATA_KEY_RDS_PS = "android.hardware.radio.metadata.RDS_PS";

    /**
     * The RDS PTY.
     */
    public static final String METADATA_KEY_RDS_PTY = "android.hardware.radio.metadata.RDS_PTY";

    /**
     * The RBDS PTY.
     */
    public static final String METADATA_KEY_RBDS_PTY = "android.hardware.radio.metadata.RBDS_PTY";

    /**
     * The RBDS Radio Text.
     */
    public static final String METADATA_KEY_RDS_RT = "android.hardware.radio.metadata.RDS_RT";

    /**
     * The song title.
     */
    public static final String METADATA_KEY_TITLE = "android.hardware.radio.metadata.TITLE";

    /**
     * The artist name.
     */
    public static final String METADATA_KEY_ARTIST = "android.hardware.radio.metadata.ARTIST";

    /**
     * The album name.
     */
    public static final String METADATA_KEY_ALBUM = "android.hardware.radio.metadata.ALBUM";

    /**
     * The music genre.
     */
    public static final String METADATA_KEY_GENRE = "android.hardware.radio.metadata.GENRE";

    /**
     * The radio station icon {@link Bitmap}.
     */
    public static final String METADATA_KEY_ICON = "android.hardware.radio.metadata.ICON";

    /**
     * The artwork for the song/album {@link Bitmap}.
     */
    public static final String METADATA_KEY_ART = "android.hardware.radio.metadata.ART";

    /**
     * The clock.
     */
    public static final String METADATA_KEY_CLOCK = "android.hardware.radio.metadata.CLOCK";

    /**
     * Technology-independent program name (station name).
     */
    public static final String METADATA_KEY_PROGRAM_NAME =
            "android.hardware.radio.metadata.PROGRAM_NAME";

    /**
     * DAB ensemble name.
     */
    public static final String METADATA_KEY_DAB_ENSEMBLE_NAME =
            "android.hardware.radio.metadata.DAB_ENSEMBLE_NAME";

    /**
     * DAB ensemble name - short version (up to 8 characters).
     */
    public static final String METADATA_KEY_DAB_ENSEMBLE_NAME_SHORT =
            "android.hardware.radio.metadata.DAB_ENSEMBLE_NAME_SHORT";

    /**
     * DAB service name.
     */
    public static final String METADATA_KEY_DAB_SERVICE_NAME =
            "android.hardware.radio.metadata.DAB_SERVICE_NAME";

    /**
     * DAB service name - short version (up to 8 characters).
     */
    public static final String METADATA_KEY_DAB_SERVICE_NAME_SHORT =
            "android.hardware.radio.metadata.DAB_SERVICE_NAME_SHORT";

    /**
     * DAB component name.
     */
    public static final String METADATA_KEY_DAB_COMPONENT_NAME =
            "android.hardware.radio.metadata.DAB_COMPONENT_NAME";

    /**
     * DAB component name.
     */
    public static final String METADATA_KEY_DAB_COMPONENT_NAME_SHORT =
            "android.hardware.radio.metadata.DAB_COMPONENT_NAME_SHORT";

    /**
     * Short context description of comment
     *
     * <p>Comment could relate to the current audio program content, or it might
     * be unrelated information that the station chooses to send. It is composed
     * of short content description and actual text (see NRSC-G200-A and id3v2.3.0
     * for more info).
     */
    @FlaggedApi(Flags.FLAG_HD_RADIO_IMPROVED)
    public static final String METADATA_KEY_COMMENT_SHORT_DESCRIPTION =
            "android.hardware.radio.metadata.COMMENT_SHORT_DESCRIPTION";

    /**
     * Actual text of comment
     *
     * @see #METADATA_KEY_COMMENT_SHORT_DESCRIPTION
     */
    @FlaggedApi(Flags.FLAG_HD_RADIO_IMPROVED)
    public static final String METADATA_KEY_COMMENT_ACTUAL_TEXT =
            "android.hardware.radio.metadata.COMMENT_ACTUAL_TEXT";

    /**
     * Commercial
     *
     * <p>Commercial is application specific and generally used to facilitate the
     * sale of products and services (see NRSC-G200-A and id3v2.3.0 for more info).
     */
    @FlaggedApi(Flags.FLAG_HD_RADIO_IMPROVED)
    public static final String METADATA_KEY_COMMERCIAL =
            "android.hardware.radio.metadata.COMMERCIAL";

    /**
     * Array of Unique File Identifiers
     *
     * <p>Unique File Identifier (UFID) can be used to transmit an alphanumeric
     * identifier of the current content, or of an advertised product or
     * service (see NRSC-G200-A and id3v2.3.0 for more info).
     */
    @FlaggedApi(Flags.FLAG_HD_RADIO_IMPROVED)
    public static final String METADATA_KEY_UFIDS = "android.hardware.radio.metadata.UFIDS";

    /**
     * HD short station name or HD universal short station name
     *
     * <p>It can be up to 12 characters (see SY_IDD_1020s for more info).
     */
    @FlaggedApi(Flags.FLAG_HD_RADIO_IMPROVED)
    public static final String METADATA_KEY_HD_STATION_NAME_SHORT =
            "android.hardware.radio.metadata.HD_STATION_NAME_SHORT";

    /**
     * HD long station name, HD station slogan or HD station message
     *
     * <p>(see SY_IDD_1020s for more info)
     */
    @FlaggedApi(Flags.FLAG_HD_RADIO_IMPROVED)
    public static final String METADATA_KEY_HD_STATION_NAME_LONG =
            "android.hardware.radio.metadata.HD_STATION_NAME_LONG";

    /**
     * Bit mask of all HD Radio subchannels available
     *
     * <p>Bit {@link ProgramSelector#SUB_CHANNEL_HD_1} from LSB represents the
     * availability of HD-1 subchannel (main program service, MPS). Bits
     * {@link ProgramSelector#SUB_CHANNEL_HD_2} to {@link ProgramSelector#SUB_CHANNEL_HD_8}
     * from LSB represent HD-2 to HD-8 subchannel (supplemental program services, SPS)
     * respectively.
     */
    @FlaggedApi(Flags.FLAG_HD_RADIO_IMPROVED)
    public static final String METADATA_KEY_HD_SUBCHANNELS_AVAILABLE =
            "android.hardware.radio.metadata.HD_SUBCHANNELS_AVAILABLE";

    private static final int METADATA_TYPE_INVALID = -1;
    private static final int METADATA_TYPE_INT = 0;
    private static final int METADATA_TYPE_TEXT = 1;
    private static final int METADATA_TYPE_BITMAP = 2;
    private static final int METADATA_TYPE_CLOCK = 3;
    private static final int METADATA_TYPE_TEXT_ARRAY = 4;

    private static final ArrayMap<String, Integer> METADATA_KEYS_TYPE;

    static {
        METADATA_KEYS_TYPE = new ArrayMap<String, Integer>();
        METADATA_KEYS_TYPE.put(METADATA_KEY_RDS_PI, METADATA_TYPE_INT);
        METADATA_KEYS_TYPE.put(METADATA_KEY_RDS_PS, METADATA_TYPE_TEXT);
        METADATA_KEYS_TYPE.put(METADATA_KEY_RDS_PTY, METADATA_TYPE_INT);
        METADATA_KEYS_TYPE.put(METADATA_KEY_RBDS_PTY, METADATA_TYPE_INT);
        METADATA_KEYS_TYPE.put(METADATA_KEY_RDS_RT, METADATA_TYPE_TEXT);
        METADATA_KEYS_TYPE.put(METADATA_KEY_TITLE, METADATA_TYPE_TEXT);
        METADATA_KEYS_TYPE.put(METADATA_KEY_ARTIST, METADATA_TYPE_TEXT);
        METADATA_KEYS_TYPE.put(METADATA_KEY_ALBUM, METADATA_TYPE_TEXT);
        METADATA_KEYS_TYPE.put(METADATA_KEY_GENRE, METADATA_TYPE_TEXT);
        METADATA_KEYS_TYPE.put(METADATA_KEY_ICON, METADATA_TYPE_BITMAP);
        METADATA_KEYS_TYPE.put(METADATA_KEY_ART, METADATA_TYPE_BITMAP);
        METADATA_KEYS_TYPE.put(METADATA_KEY_CLOCK, METADATA_TYPE_CLOCK);
        METADATA_KEYS_TYPE.put(METADATA_KEY_PROGRAM_NAME, METADATA_TYPE_TEXT);
        METADATA_KEYS_TYPE.put(METADATA_KEY_DAB_ENSEMBLE_NAME, METADATA_TYPE_TEXT);
        METADATA_KEYS_TYPE.put(METADATA_KEY_DAB_ENSEMBLE_NAME_SHORT, METADATA_TYPE_TEXT);
        METADATA_KEYS_TYPE.put(METADATA_KEY_DAB_SERVICE_NAME, METADATA_TYPE_TEXT);
        METADATA_KEYS_TYPE.put(METADATA_KEY_DAB_SERVICE_NAME_SHORT, METADATA_TYPE_TEXT);
        METADATA_KEYS_TYPE.put(METADATA_KEY_DAB_COMPONENT_NAME, METADATA_TYPE_TEXT);
        METADATA_KEYS_TYPE.put(METADATA_KEY_DAB_COMPONENT_NAME_SHORT, METADATA_TYPE_TEXT);
        METADATA_KEYS_TYPE.put(METADATA_KEY_COMMENT_SHORT_DESCRIPTION, METADATA_TYPE_TEXT);
        METADATA_KEYS_TYPE.put(METADATA_KEY_COMMENT_ACTUAL_TEXT, METADATA_TYPE_TEXT);
        METADATA_KEYS_TYPE.put(METADATA_KEY_COMMERCIAL, METADATA_TYPE_TEXT);
        METADATA_KEYS_TYPE.put(METADATA_KEY_UFIDS, METADATA_TYPE_TEXT_ARRAY);
        METADATA_KEYS_TYPE.put(METADATA_KEY_HD_STATION_NAME_SHORT, METADATA_TYPE_TEXT);
        METADATA_KEYS_TYPE.put(METADATA_KEY_HD_STATION_NAME_LONG, METADATA_TYPE_TEXT);
        METADATA_KEYS_TYPE.put(METADATA_KEY_HD_SUBCHANNELS_AVAILABLE, METADATA_TYPE_INT);
    }

    // keep in sync with: system/media/radio/include/system/radio_metadata.h
    private static final int NATIVE_KEY_INVALID     = -1;
    private static final int NATIVE_KEY_RDS_PI      = 0;
    private static final int NATIVE_KEY_RDS_PS      = 1;
    private static final int NATIVE_KEY_RDS_PTY     = 2;
    private static final int NATIVE_KEY_RBDS_PTY    = 3;
    private static final int NATIVE_KEY_RDS_RT      = 4;
    private static final int NATIVE_KEY_TITLE       = 5;
    private static final int NATIVE_KEY_ARTIST      = 6;
    private static final int NATIVE_KEY_ALBUM       = 7;
    private static final int NATIVE_KEY_GENRE       = 8;
    private static final int NATIVE_KEY_ICON        = 9;
    private static final int NATIVE_KEY_ART         = 10;
    private static final int NATIVE_KEY_CLOCK       = 11;

    private static final SparseArray<String> NATIVE_KEY_MAPPING;

    static {
        NATIVE_KEY_MAPPING = new SparseArray<String>();
        NATIVE_KEY_MAPPING.put(NATIVE_KEY_RDS_PI, METADATA_KEY_RDS_PI);
        NATIVE_KEY_MAPPING.put(NATIVE_KEY_RDS_PS, METADATA_KEY_RDS_PS);
        NATIVE_KEY_MAPPING.put(NATIVE_KEY_RDS_PTY, METADATA_KEY_RDS_PTY);
        NATIVE_KEY_MAPPING.put(NATIVE_KEY_RBDS_PTY, METADATA_KEY_RBDS_PTY);
        NATIVE_KEY_MAPPING.put(NATIVE_KEY_RDS_RT, METADATA_KEY_RDS_RT);
        NATIVE_KEY_MAPPING.put(NATIVE_KEY_TITLE, METADATA_KEY_TITLE);
        NATIVE_KEY_MAPPING.put(NATIVE_KEY_ARTIST, METADATA_KEY_ARTIST);
        NATIVE_KEY_MAPPING.put(NATIVE_KEY_ALBUM, METADATA_KEY_ALBUM);
        NATIVE_KEY_MAPPING.put(NATIVE_KEY_GENRE, METADATA_KEY_GENRE);
        NATIVE_KEY_MAPPING.put(NATIVE_KEY_ICON, METADATA_KEY_ICON);
        NATIVE_KEY_MAPPING.put(NATIVE_KEY_ART, METADATA_KEY_ART);
        NATIVE_KEY_MAPPING.put(NATIVE_KEY_CLOCK, METADATA_KEY_CLOCK);
    }

    /**
     * Provides a Clock that can be used to describe time as provided by the Radio.
     *
     * <p>The clock time is defined by the seconds since epoch at the UTC + 0 timezone
     * and timezone offset from UTC + 0 represented in number of minutes.
     *
     * @hide
     */
    @SystemApi
    public static final class Clock implements Parcelable {
        private final long mUtcEpochSeconds;
        private final int mTimezoneOffsetMinutes;

        public int describeContents() {
            return 0;
        }

        public void writeToParcel(Parcel out, int flags) {
            out.writeLong(mUtcEpochSeconds);
            out.writeInt(mTimezoneOffsetMinutes);
        }

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

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

        public Clock(long utcEpochSeconds, int timezoneOffsetMinutes) {
            mUtcEpochSeconds = utcEpochSeconds;
            mTimezoneOffsetMinutes = timezoneOffsetMinutes;
        }

        private Clock(Parcel in) {
            mUtcEpochSeconds = in.readLong();
            mTimezoneOffsetMinutes = in.readInt();
        }

        public long getUtcEpochSeconds() {
            return mUtcEpochSeconds;
        }

        public int getTimezoneOffsetMinutes() {
            return mTimezoneOffsetMinutes;
        }
    }

    private final Bundle mBundle;

    // Lazily computed hash code based upon the contents of mBundle.
    private Integer mHashCode;

    @Override
    public int hashCode() {
        if (mHashCode == null) {
            List<String> keys = new ArrayList<String>(mBundle.keySet());
            keys.sort(null);
            Object[] objs = new Object[2 * keys.size()];
            for (int i = 0; i < keys.size(); i++) {
                objs[2 * i] = keys.get(i);
                objs[2 * i + 1] = mBundle.get(keys.get(i));
            }
            mHashCode = Arrays.hashCode(objs);
        }
        return mHashCode;
    }

    @Override
    public boolean equals(@Nullable Object obj) {
        if (this == obj) return true;
        if (!(obj instanceof RadioMetadata)) return false;
        Bundle otherBundle = ((RadioMetadata) obj).mBundle;
        if (!mBundle.keySet().equals(otherBundle.keySet())) {
            return false;
        }
        for (String key : mBundle.keySet()) {
            if (Flags.hdRadioImproved() && Objects.equals(METADATA_KEYS_TYPE.get(key),
                    METADATA_TYPE_TEXT_ARRAY)) {
                if (!Arrays.equals(mBundle.getStringArray(key), otherBundle.getStringArray(key))) {
                    return false;
                }
            } else if (!Objects.equals(mBundle.get(key), otherBundle.get(key))) {
                return false;
            }
        }
        return true;
    }

    RadioMetadata() {
        mBundle = new Bundle();
    }

    private RadioMetadata(Bundle bundle) {
        mBundle = new Bundle(bundle);
    }

    private RadioMetadata(Parcel in) {
        mBundle = in.readBundle();
    }

    @NonNull
    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder("RadioMetadata[");

        final String removePrefix = "android.hardware.radio.metadata";

        boolean first = true;
        for (String key : mBundle.keySet()) {
            if (first) first = false;
            else sb.append(", ");

            String keyDisp = key;
            if (key.startsWith(removePrefix)) keyDisp = key.substring(removePrefix.length());

            sb.append(keyDisp);
            sb.append('=');
            if (Flags.hdRadioImproved() && Objects.equals(METADATA_KEYS_TYPE.get(key),
                    METADATA_TYPE_TEXT_ARRAY)) {
                String[] stringArrayValue = mBundle.getStringArray(key);
                sb.append('[');
                for (int i = 0; i < stringArrayValue.length; i++) {
                    if (i != 0) {
                        sb.append(',');
                    }
                    sb.append(stringArrayValue[i]);
                }
                sb.append(']');
            } else {
                sb.append(mBundle.get(key));
            }

        }

        sb.append("]");
        return sb.toString();
    }

    /**
     * Returns {@code true} if the given key is contained in the meta data
     *
     * @param key a String key
     * @return {@code true} if the key exists in this meta data, {@code false} otherwise
     */
    public boolean containsKey(String key) {
        return mBundle.containsKey(key);
    }

    /**
     * Returns the text value associated with the given key as a String, or null
     * if the key is not found in the meta data.
     *
     * @param key The key the value is stored under
     * @return a String value, or null
     */
    public String getString(String key) {
        return mBundle.getString(key);
    }

    private static void putInt(Bundle bundle, String key, int value) {
        int type = METADATA_KEYS_TYPE.getOrDefault(key, METADATA_TYPE_INVALID);
        if (type != METADATA_TYPE_INT && type != METADATA_TYPE_BITMAP) {
            throw new IllegalArgumentException("The " + key + " key cannot be used to put an int");
        }
        bundle.putInt(key, value);
    }

    /**
     * Returns the value associated with the given key,
     * or 0 if the key is not found in the meta data.
     *
     * @param key The key the value is stored under
     * @return an int value
     */
    public int getInt(String key) {
        return mBundle.getInt(key, 0);
    }

    /**
     * Returns a {@link Bitmap} for the given key or null if the key is not found in the meta data.
     *
     * @param key The key the value is stored under
     * @return a {@link Bitmap} or null
     * @deprecated Use getBitmapId(String) instead
     */
    @Deprecated
    public Bitmap getBitmap(String key) {
        Bitmap bmp = null;
        try {
            bmp = mBundle.getParcelable(key, android.graphics.Bitmap.class);
        } catch (Exception e) {
            // ignore, value was not a bitmap
            Log.w(TAG, "Failed to retrieve a key as Bitmap.", e);
        }
        return bmp;
    }

    /**
     * Retrieves an identifier for a bitmap.
     *
     * <p>The format of an identifier is opaque to the application,
     * with a special case of value 0 being invalid.
     * An identifier for a given image-tuner pair is unique, so an application
     * may cache images and determine if there is a necessity to fetch them
     * again - if identifier changes, it means the image has changed.
     *
     * <p>Only bitmap keys may be used with this method:
     * <ul>
     *     <li>{@link #METADATA_KEY_ICON}</li>
     *     <li>{@link #METADATA_KEY_ART}</li>
     * </ul>
     *
     * @param key The key the value is stored under.
     * @return a bitmap identifier or 0 if it's missing.
     * @throws NullPointerException if metadata key is {@code null}
     * @throws IllegalArgumentException if the metadata with the key is not found in
     * metadata or the key is not of bitmap-key type
     */
    @FlaggedApi(Flags.FLAG_HD_RADIO_IMPROVED)
    public int getBitmapId(@NonNull String key) {
        Objects.requireNonNull(key, "Metadata key can not be null");
        if (!METADATA_KEY_ICON.equals(key) && !METADATA_KEY_ART.equals(key)) {
            throw new IllegalArgumentException("Failed to retrieve key " + key + " as bitmap key");
        }
        return getInt(key);
    }

    public Clock getClock(String key) {
        Clock clock = null;
        try {
            clock = mBundle.getParcelable(key, android.hardware.radio.RadioMetadata.Clock.class);
        } catch (Exception e) {
            // ignore, value was not a clock.
            Log.w(TAG, "Failed to retrieve a key as Clock.", e);
        }
        return clock;
    }

    /**
     * Gets the string array value associated with the given key as a string
     * array.
     *
     * <p>Only string array keys may be used with this method:
     * <ul>
     *     <li>{@link #METADATA_KEY_UFIDS}</li>
     * </ul>
     *
     * @param key The key the value is stored under
     * @return String array of the given string-array-type key
     * @throws NullPointerException if metadata key is {@code null}
     * @throws IllegalArgumentException if the metadata with the key is not found in
     * metadata or the key is not of string-array type
     */
    @FlaggedApi(Flags.FLAG_HD_RADIO_IMPROVED)
    @NonNull
    public String[] getStringArray(@NonNull String key) {
        Objects.requireNonNull(key, "Metadata key can not be null");
        if (!Objects.equals(METADATA_KEYS_TYPE.get(key), METADATA_TYPE_TEXT_ARRAY)) {
            throw new IllegalArgumentException("Failed to retrieve key " + key
                    + " as string array");
        }
        String[] stringArrayValue = mBundle.getStringArray(key);
        if (stringArrayValue == null) {
            throw new IllegalArgumentException("Key " + key + " is not found in metadata");
        }
        return stringArrayValue;
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeBundle(mBundle);
    }

    /**
     * Returns the number of fields in this meta data.
     *
     * @return the number of fields in the meta data.
     */
    public int size() {
        return mBundle.size();
    }

    /**
     * Returns a Set containing the Strings used as keys in this meta data.
     *
     * @return a Set of String keys
     */
    public Set<String> keySet() {
        return mBundle.keySet();
    }

    /**
     * Helper for getting the String key used by {@link RadioMetadata} from the
     * corrsponding native integer key.
     *
     * @param nativeKey The key used by the editor
     * @return the key used by this class or null if no mapping exists
     * @hide
     */
    public static String getKeyFromNativeKey(int nativeKey) {
        return NATIVE_KEY_MAPPING.get(nativeKey, null);
    }

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

                @Override
                public RadioMetadata[] newArray(int size) {
                    return new RadioMetadata[size];
                }
            };

    /**
     * Use to build RadioMetadata objects.
     */
    public static final class Builder {
        private final Bundle mBundle;

        /**
         * Create an empty Builder. Any field that should be included in the
         * {@link RadioMetadata} must be added.
         */
        public Builder() {
            mBundle = new Bundle();
        }

        /**
         * Create a Builder using a {@link RadioMetadata} instance to set the
         * initial values. All fields in the source meta data will be included in
         * the new meta data. Fields can be overwritten by adding the same key.
         *
         * @param source
         */
        public Builder(RadioMetadata source) {
            mBundle = new Bundle(source.mBundle);
        }

        /**
         * Create a Builder using a {@link RadioMetadata} instance to set
         * initial values, but replace bitmaps with a scaled down copy if they
         * are larger than maxBitmapSize.
         *
         * @param source The original meta data to copy.
         * @param maxBitmapSize The maximum height/width for bitmaps contained
         *            in the meta data.
         * @hide
         */
        public Builder(RadioMetadata source, int maxBitmapSize) {
            this(source);
            for (String key : mBundle.keySet()) {
                Object value = mBundle.get(key);
                if (value != null && value instanceof Bitmap) {
                    Bitmap bmp = (Bitmap) value;
                    if (bmp.getHeight() > maxBitmapSize || bmp.getWidth() > maxBitmapSize) {
                        putBitmap(key, scaleBitmap(bmp, maxBitmapSize));
                    }
                }
            }
        }

        /**
         * Put a String value into the meta data. Custom keys may be used, but if
         * the METADATA_KEYs defined in this class are used they may only be one
         * of the following:
         * <ul>
         *     <li>{@link #METADATA_KEY_RDS_PS}</li>
         *     <li>{@link #METADATA_KEY_RDS_RT}</li>
         *     <li>{@link #METADATA_KEY_TITLE}</li>
         *     <li>{@link #METADATA_KEY_ARTIST}</li>
         *     <li>{@link #METADATA_KEY_ALBUM}</li>
         *     <li>{@link #METADATA_KEY_GENRE}</li>
         *     <li>{@link #METADATA_KEY_COMMENT_SHORT_DESCRIPTION}</li>
         *     <li>{@link #METADATA_KEY_COMMENT_ACTUAL_TEXT}</li>
         *     <li>{@link #METADATA_KEY_COMMERCIAL}</li>
         *     <li>{@link #METADATA_KEY_HD_STATION_NAME_SHORT}</li>
         *     <li>{@link #METADATA_KEY_HD_STATION_NAME_LONG}</li>
         * </ul>
         *
         * @param key The key for referencing this value
         * @param value The String value to store
         * @return the same Builder instance
         */
        public Builder putString(String key, String value) {
            if (!METADATA_KEYS_TYPE.containsKey(key) ||
                    METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_TEXT) {
                throw new IllegalArgumentException("The " + key
                        + " key cannot be used to put a String");
            }
            mBundle.putString(key, value);
            return this;
        }

        /**
         * Put an int value into the meta data. Custom keys may be used, but if
         * the METADATA_KEYs defined in this class are used they may only be one
         * of the following:
         * <ul>
         *     <li>{@link #METADATA_KEY_RDS_PI}</li>
         *     <li>{@link #METADATA_KEY_RDS_PTY}</li>
         *     <li>{@link #METADATA_KEY_RBDS_PTY}</li>
         *     <li>{@link #METADATA_KEY_HD_SUBCHANNELS_AVAILABLE}</li>
         * </ul>
         * or any bitmap represented by its identifier.
         *
         * @param key The key for referencing this value
         * @param value The int value to store
         * @return the same Builder instance
         */
        public Builder putInt(String key, int value) {
            RadioMetadata.putInt(mBundle, key, value);
            return this;
        }

        /**
         * Put a {@link Bitmap} into the meta data. Custom keys may be used, but
         * if the METADATA_KEYs defined in this class are used they may only be
         * one of the following:
         * <ul>
         *     <li>{@link #METADATA_KEY_ICON}</li>
         *     <li>{@link #METADATA_KEY_ART}</li>
         * </ul>
         * <p>
         *
         * @param key The key for referencing this value
         * @param value The Bitmap to store
         * @return the same Builder instance
         */
        public Builder putBitmap(String key, Bitmap value) {
            if (!METADATA_KEYS_TYPE.containsKey(key) ||
                    METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_BITMAP) {
                throw new IllegalArgumentException("The " + key
                        + " key cannot be used to put a Bitmap");
            }
            mBundle.putParcelable(key, value);
            return this;
        }

        /**
         * Put a {@link RadioMetadata.Clock} into the meta data. Custom keys may be used, but if the
         * METADATA_KEYs defined in this class are used they may only be one of the following:
         * <ul>
         * <li>{@link #METADATA_KEY_CLOCK}</li>
         * </ul>
         *
         * @param utcSecondsSinceEpoch Number of seconds since epoch for UTC + 0 timezone.
         * @param timezoneOffsetMinutes Offset of timezone from UTC + 0 in minutes.
         * @return the same Builder instance.
         */
        public Builder putClock(String key, long utcSecondsSinceEpoch, int timezoneOffsetMinutes) {
            if (!METADATA_KEYS_TYPE.containsKey(key) ||
                    METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_CLOCK) {
                throw new IllegalArgumentException("The " + key
                    + " key cannot be used to put a RadioMetadata.Clock.");
            }
            mBundle.putParcelable(key, new Clock(utcSecondsSinceEpoch, timezoneOffsetMinutes));
            return this;
        }

        /**
         * Put a String array into the meta data. Custom keys may be used, but if
         * the METADATA_KEYs defined in this class are used they may only be one
         * of the following:
         * <ul>
         *     <li>{@link #METADATA_KEY_UFIDS}</li>
         * </ul>
         *
         * @param key The key for referencing this value
         * @param value The String value to store
         * @return the same Builder instance
         * @throws NullPointerException if key or value is null
         * @throws IllegalArgumentException if the key is not string-array-type key
         */
        @FlaggedApi(Flags.FLAG_HD_RADIO_IMPROVED)
        @NonNull
        public Builder putStringArray(@NonNull String key, @NonNull String[] value) {
            Objects.requireNonNull(key, "Key can not be null");
            Objects.requireNonNull(value, "Value can not be null");
            if (!METADATA_KEYS_TYPE.containsKey(key)
                    || !Objects.equals(METADATA_KEYS_TYPE.get(key), METADATA_TYPE_TEXT_ARRAY)) {
                throw new IllegalArgumentException("The " + key
                        + " key cannot be used to put a RadioMetadata String Array.");
            }
            mBundle.putStringArray(key, value);
            return this;
        }


        /**
         * Creates a {@link RadioMetadata} instance with the specified fields.
         *
         * @return a new {@link RadioMetadata} object
         */
        public RadioMetadata build() {
            return new RadioMetadata(mBundle);
        }

        private Bitmap scaleBitmap(Bitmap bmp, int maxSize) {
            float maxSizeF = maxSize;
            float widthScale = maxSizeF / bmp.getWidth();
            float heightScale = maxSizeF / bmp.getHeight();
            float scale = Math.min(widthScale, heightScale);
            int height = (int) (bmp.getHeight() * scale);
            int width = (int) (bmp.getWidth() * scale);
            return Bitmap.createScaledBitmap(bmp, width, height, true);
        }
    }

    int putIntFromNative(int nativeKey, int value) {
        String key = getKeyFromNativeKey(nativeKey);
        try {
            putInt(mBundle, key, value);
            // Invalidate mHashCode to force it to be recomputed.
            mHashCode = null;
            return 0;
        } catch (IllegalArgumentException ex) {
            return -1;
        }
    }

    int putStringFromNative(int nativeKey, String value) {
        String key = getKeyFromNativeKey(nativeKey);
        if (!METADATA_KEYS_TYPE.containsKey(key) ||
                METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_TEXT) {
            return -1;
        }
        mBundle.putString(key, value);
        // Invalidate mHashCode to force it to be recomputed.
        mHashCode = null;
        return 0;
    }

    int putBitmapFromNative(int nativeKey, byte[] value) {
        String key = getKeyFromNativeKey(nativeKey);
        if (!METADATA_KEYS_TYPE.containsKey(key) ||
                METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_BITMAP) {
            return -1;
        }
        Bitmap bmp = null;
        try {
            bmp = BitmapFactory.decodeByteArray(value, 0, value.length);
            if (bmp != null) {
                mBundle.putParcelable(key, bmp);
                // Invalidate mHashCode to force it to be recomputed.
                mHashCode = null;
                return 0;
            }
        } catch (Exception e) {
        }
        return -1;
    }

    int putClockFromNative(int nativeKey, long utcEpochSeconds, int timezoneOffsetInMinutes) {
        String key = getKeyFromNativeKey(nativeKey);
        if (!METADATA_KEYS_TYPE.containsKey(key) ||
                METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_CLOCK) {
              return -1;
        }
        mBundle.putParcelable(key, new RadioMetadata.Clock(
            utcEpochSeconds, timezoneOffsetInMinutes));
        // Invalidate mHashCode to force it to be recomputed.
        mHashCode = null;
        return 0;
    }
}
