content: /**
   * 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.Manifest;
  import android.annotation.CallbackExecutor;
  import android.annotation.IntDef;
  import android.annotation.NonNull;
  import android.annotation.Nullable;
  import android.annotation.RequiresFeature;
  import android.annotation.RequiresPermission;
  import android.annotation.SystemApi;
  import android.annotation.SystemService;
  import android.content.Context;
  import android.content.pm.PackageManager;
  import android.os.Handler;
  import android.os.Parcel;
  import android.os.Parcelable;
  import android.os.RemoteException;
  import android.os.ServiceManager;
  import android.os.ServiceManager.ServiceNotFoundException;
  import android.text.TextUtils;
  import android.util.Log;
  
  import com.android.internal.util.Preconditions;
  
  import java.lang.annotation.Retention;
  import java.lang.annotation.RetentionPolicy;
  import java.util.Arrays;
  import java.util.Collection;
  import java.util.Collections;
  import java.util.HashMap;
  import java.util.List;
  import java.util.Map;
  import java.util.Objects;
  import java.util.Set;
  import java.util.concurrent.Executor;
  import java.util.stream.Collectors;
  
  /**
   * The RadioManager class allows to control a broadcast radio tuner present on the device.
   * It provides data structures and methods to query for available radio modules, list their
   * properties and open an interface to control tuning operations and receive callbacks when
   * asynchronous operations complete or events occur.
   * @hide
   */
  @SystemApi
  @SystemService(Context.RADIO_SERVICE)
  @RequiresFeature(PackageManager.FEATURE_BROADCAST_RADIO)
  public class RadioManager {
      private static final String TAG = "BroadcastRadio.manager";
  
      /** Method return status: successful operation */
      public static final int STATUS_OK = 0;
      /** Method return status: unspecified error */
      public static final int STATUS_ERROR = Integer.MIN_VALUE;
      /** Method return status: permission denied */
      public static final int STATUS_PERMISSION_DENIED = -1;
      /** Method return status: initialization failure */
      public static final int STATUS_NO_INIT = -19;
      /** Method return status: invalid argument provided */
      public static final int STATUS_BAD_VALUE = -22;
      /** Method return status: cannot reach service */
      public static final int STATUS_DEAD_OBJECT = -32;
      /** Method return status: invalid or out of sequence operation */
      public static final int STATUS_INVALID_OPERATION = -38;
      /** Method return status: time out before operation completion */
      public static final int STATUS_TIMED_OUT = -110;
  
  
      // keep in sync with radio_class_t in /system/core/incluse/system/radio.h
      /** Radio module class supporting FM (including HD radio) and AM */
      public static final int CLASS_AM_FM = 0;
      /** Radio module class supporting satellite radio */
      public static final int CLASS_SAT = 1;
      /** Radio module class supporting Digital terrestrial radio */
      public static final int CLASS_DT = 2;
  
      public static final int BAND_INVALID = -1;
      /** AM radio band (LW/MW/SW).
       * @see BandDescriptor */
      public static final int BAND_AM = 0;
      /** FM radio band.
       * @see BandDescriptor */
      public static final int BAND_FM = 1;
      /** FM HD radio or DRM  band.
       * @see BandDescriptor */
      public static final int BAND_FM_HD = 2;
      /** AM HD radio or DRM band.
       * @see BandDescriptor */
      public static final int BAND_AM_HD = 3;
      @IntDef(prefix = { "BAND_" }, value = {
          BAND_INVALID,
          BAND_AM,
          BAND_FM,
          BAND_AM_HD,
          BAND_FM_HD,
      })
      @Retention(RetentionPolicy.SOURCE)
      public @interface Band {}
  
      // keep in sync with radio_region_t in /system/core/incluse/system/radio.h
      /** Africa, Europe.
       * @see BandDescriptor */
      public static final int REGION_ITU_1  = 0;
      /** Americas.
       * @see BandDescriptor */
      public static final int REGION_ITU_2  = 1;
      /** Russia.
       * @see BandDescriptor */
      public static final int REGION_OIRT   = 2;
      /** Japan.
       * @see BandDescriptor */
      public static final int REGION_JAPAN  = 3;
      /** Korea.
       * @see BandDescriptor */
      public static final int REGION_KOREA  = 4;
  
      /**
       * Forces mono audio stream reception.
       *
       * Analog broadcasts can recover poor reception conditions by jointing
       * stereo channels into one. Mainly for, but not limited to AM/FM.
       */
      public static final int CONFIG_FORCE_MONO = 1;
      /**
       * Forces the analog playback for the supporting radio technology.
       *
       * User may disable digital playback for FM HD Radio or hybrid FM/DAB with
       * this option. This is purely user choice, ie. does not reflect digital-
       * analog handover state managed from the HAL implementation side.
       *
       * Some radio technologies may not support this, ie. DAB.
       */
      public static final int CONFIG_FORCE_ANALOG = 2;
      /**
       * Forces the digital playback for the supporting radio technology.
       *
       * User may disable digital-analog handover that happens with poor
       * reception conditions. With digital forced, the radio will remain silent
       * instead of switching to analog channel if it's available. This is purely
       * user choice, it does not reflect the actual state of handover.
       */
      public static final int CONFIG_FORCE_DIGITAL = 3;
      /**
       * RDS Alternative Frequencies.
       *
       * If set and the currently tuned RDS station broadcasts on multiple
       * channels, radio tuner automatically switches to the best available
       * alternative.
       */
      public static final int CONFIG_RDS_AF = 4;
      /**
       * RDS region-specific program lock-down.
       *
       * Allows user to lock to the current region as they move into the
       * other region.
       */
      public static final int CONFIG_RDS_REG = 5;
      /** Enables DAB-DAB hard- and implicit-linking (the same content). */
      public static final int CONFIG_DAB_DAB_LINKING = 6;
      /** Enables DAB-FM hard- and implicit-linking (the same content). */
      public static final int CONFIG_DAB_FM_LINKING = 7;
      /** Enables DAB-DAB soft-linking (related content). */
      public static final int CONFIG_DAB_DAB_SOFT_LINKING = 8;
      /** Enables DAB-FM soft-linking (related content). */
      public static final int CONFIG_DAB_FM_SOFT_LINKING = 9;
  
      /** @hide */
      @IntDef(prefix = { "CONFIG_" }, value = {
          CONFIG_FORCE_MONO,
          CONFIG_FORCE_ANALOG,
          CONFIG_FORCE_DIGITAL,
          CONFIG_RDS_AF,
          CONFIG_RDS_REG,
          CONFIG_DAB_DAB_LINKING,
          CONFIG_DAB_FM_LINKING,
          CONFIG_DAB_DAB_SOFT_LINKING,
          CONFIG_DAB_FM_SOFT_LINKING,
      })
      @Retention(RetentionPolicy.SOURCE)
      public @interface ConfigFlag {}
  
      /*****************************************************************************
       * Lists properties, options and radio bands supported by a given broadcast radio module.
       * Each module has a unique ID used to address it when calling RadioManager APIs.
       * Module properties are returned by {@link #listModules(List <ModuleProperties>)} method.
       ****************************************************************************/
      public static class ModuleProperties implements Parcelable {
  
          private final int mId;
          @NonNull private final String mServiceName;
          private final int mClassId;
          private final String mImplementor;
          private final String mProduct;
          private final String mVersion;
          private final String mSerial;
          private final int mNumTuners;
          private final int mNumAudioSources;
          private final boolean mIsInitializationRequired;
          private final boolean mIsCaptureSupported;
          private final BandDescriptor[] mBands;
          private final boolean mIsBgScanSupported;
          private final Set<Integer> mSupportedProgramTypes;
          private final Set<Integer> mSupportedIdentifierTypes;
          @Nullable private final Map<String, Integer> mDabFrequencyTable;
          @NonNull private final Map<String, String> mVendorInfo;
  
          /** @hide */
          public ModuleProperties(int id, String serviceName, int classId, String implementor,
                  String product, String version, String serial, int numTuners, int numAudioSources,
                  boolean isInitializationRequired, boolean isCaptureSupported,
                  BandDescriptor[] bands, boolean isBgScanSupported,
                  @ProgramSelector.ProgramType int[] supportedProgramTypes,
                  @ProgramSelector.IdentifierType int[] supportedIdentifierTypes,
                  @Nullable Map<String, Integer> dabFrequencyTable,
                  Map<String, String> vendorInfo) {
              mId = id;
              mServiceName = TextUtils.isEmpty(serviceName) ? "default" : serviceName;
              mClassId = classId;
              mImplementor = implementor;
              mProduct = product;
              mVersion = version;
              mSerial = serial;
              mNumTuners = numTuners;
              mNumAudioSources = numAudioSources;
              mIsInitializationRequired = isInitializationRequired;
              mIsCaptureSupported = isCaptureSupported;
              mBands = bands;
              mIsBgScanSupported = isBgScanSupported;
              mSupportedProgramTypes = arrayToSet(supportedProgramTypes);
              mSupportedIdentifierTypes = arrayToSet(supportedIdentifierTypes);
              if (dabFrequencyTable != null) {
                  for (Map.Entry<String, Integer> entry : dabFrequencyTable.entrySet()) {
                      Objects.requireNonNull(entry.getKey());
                      Objects.requireNonNull(entry.getValue());
                  }
              }
              mDabFrequencyTable = dabFrequencyTable;
              mVendorInfo = (vendorInfo == null) ? new HashMap<>() : vendorInfo;
          }
  
          private static Set<Integer> arrayToSet(int[] arr) {
              return Arrays.stream(arr).boxed().collect(Collectors.toSet());
          }
  
          private static int[] setToArray(Set<Integer> set) {
              return set.stream().mapToInt(Integer::intValue).toArray();
          }
  
          /** Unique module identifier provided by the native service.
           * For use with {@link #openTuner(int, BandConfig, boolean, Callback, Handler)}.
           * @return the radio module unique identifier.
           */
          public int getId() {
              return mId;
          }
  
          /**
           * Module service (driver) name as registered with HIDL.
           * @return the module service name.
           */
          public @NonNull String getServiceName() {
              return mServiceName;
          }
  
          /** Module class identifier: {@link #CLASS_AM_FM}, {@link #CLASS_SAT}, {@link #CLASS_DT}
           * @return the radio module class identifier.
           */
          public int getClassId() {
              return mClassId;
          }
  
          /** Human readable broadcast radio module implementor
           * @return the name of the radio module implementator.
           */
          public String getImplementor() {
              return mImplementor;
          }
  
          /** Human readable broadcast radio module product name
           * @return the radio module product name.
           */
          public String getProduct() {
              return mProduct;
          }
  
          /** Human readable broadcast radio module version number
           * @return the radio module version.
           */
          public String getVersion() {
              return mVersion;
          }
  
          /** Radio module serial number.
           * Can be used for subscription services.
           * @return the radio module serial number.
           */
          public String getSerial() {
              return mSerial;
          }
  
          /** Number of tuners available.
           * This is the number of tuners that can be open simultaneously.
           * @return the number of tuners supported.
           */
          public int getNumTuners() {
              return mNumTuners;
          }
  
          /** Number tuner audio sources available. Must be less or equal to getNumTuners().
           * When more than one tuner is supported, one is usually for playback and has one
           * associated audio source and the other is for pre scanning and building a
           * program list.
           * @return the number of audio sources available.
           */
          public int getNumAudioSources() {
              return mNumAudioSources;
          }
  
          /**
           * Checks, if BandConfig initialization (after {@link RadioManager#openTuner})
           * is required to be done before other operations or not.
           *
           * If it is, the client has to wait for {@link RadioTuner.Callback#onConfigurationChanged}
           * callback before executing any other operations. Otherwise, such operation will fail
           * returning {@link RadioManager#STATUS_INVALID_OPERATION} error code.
           */
          public boolean isInitializationRequired() {
              return mIsInitializationRequired;
          }
  
          /** {@code true} if audio capture is possible from radio tuner output.
           * This indicates if routing to audio devices not connected to the same HAL as the FM radio
           * is possible (e.g. to USB) or DAR (Digital Audio Recorder) feature can be implemented.
           * @return {@code true} if audio capture is possible, {@code false} otherwise.
           */
          public boolean isCaptureSupported() {
              return mIsCaptureSupported;
          }
  
          /**
           * {@code true} if the module supports background scanning. At the given time it may not
           * be available though, see {@link RadioTuner#startBackgroundScan()}.
           *
           * @return {@code true} if background scanning is supported (not necessary available
           * at a given time), {@code false} otherwise.
           */
          public boolean isBackgroundScanningSupported() {
              return mIsBgScanSupported;
          }
  
          /**
           * Checks, if a given program type is supported by this tuner.
           *
           * If a program type is supported by radio module, it means it can tune
           * to ProgramSelector of a given type.
           *
           * @return {@code true} if a given program type is supported.
           */
          public boolean isProgramTypeSupported(@ProgramSelector.ProgramType int type) {
              return mSupportedProgramTypes.contains(type);
          }
  
          /**
           * Checks, if a given program identifier is supported by this tuner.
           *
           * If an identifier is supported by radio module, it means it can use it for
           * tuning to ProgramSelector with either primary or secondary Identifier of
           * a given type.
           *
           * @return {@code true} if a given program type is supported.
           */
          public boolean isProgramIdentifierSupported(@ProgramSelector.IdentifierType int type) {
              return mSupportedIdentifierTypes.contains(type);
          }
  
          /**
           * A frequency table for Digital Audio Broadcasting (DAB).
           *
           * The key is a channel name, i.e. 5A, 7B.
           *
           * The value is a frequency, in kHz.
           *
           * @return a frequency table, or {@code null} if the module doesn't support DAB
           */
          public @Nullable Map<String, Integer> getDabFrequencyTable() {
              return mDabFrequencyTable;
          }
  
          /**
           * A map of vendor-specific opaque strings, passed from HAL without changes.
           * Format of these strings can vary across vendors.
           *
           * It may be used for extra features, that's not supported by a platform,
           * for example: preset-slots=6; ultra-hd-capable=false.
           *
           * Keys must be prefixed with unique vendor Java-style namespace,
           * eg. 'com.somecompany.parameter1'.
           */
          public @NonNull Map<String, String> getVendorInfo() {
              return mVendorInfo;
          }
  
          /** List of descriptors for all bands supported by this module.
           * @return an array of {@link BandDescriptor}.
           */
          public BandDescriptor[] getBands() {
              return mBands;
          }
  
          private ModuleProperties(Parcel in) {
              mId = in.readInt();
              String serviceName = in.readString();
              mServiceName = TextUtils.isEmpty(serviceName) ? "default" : serviceName;
              mClassId = in.readInt();
              mImplementor = in.readString();
              mProduct = in.readString();
              mVersion = in.readString();
              mSerial = in.readString();
              mNumTuners = in.readInt();
              mNumAudioSources = in.readInt();
              mIsInitializationRequired = in.readInt() == 1;
              mIsCaptureSupported = in.readInt() == 1;
              Parcelable[] tmp = in.readParcelableArray(BandDescriptor.class.getClassLoader());
              mBands = new BandDescriptor[tmp.length];
              for (int i = 0; i < tmp.length; i++) {
                  mBands[i] = (BandDescriptor) tmp[i];
              }
              mIsBgScanSupported = in.readInt() == 1;
              mSupportedProgramTypes = arrayToSet(in.createIntArray());
              mSupportedIdentifierTypes = arrayToSet(in.createIntArray());
              mDabFrequencyTable = Utils.readStringIntMap(in);
              mVendorInfo = Utils.readStringMap(in);
          }
  
          public static final @android.annotation.NonNull Parcelable.Creator<ModuleProperties> CREATOR
                  = new Parcelable.Creator<ModuleProperties>() {
              public ModuleProperties createFromParcel(Parcel in) {
                  return new ModuleProperties(in);
              }
  
              public ModuleProperties[] newArray(int size) {
                  return new ModuleProperties[size];
              }
          };
  
          @Override
          public void writeToParcel(Parcel dest, int flags) {
              dest.writeInt(mId);
              dest.writeString(mServiceName);
              dest.writeInt(mClassId);
              dest.writeString(mImplementor);
              dest.writeString(mProduct);
              dest.writeString(mVersion);
              dest.writeString(mSerial);
              dest.writeInt(mNumTuners);
              dest.writeInt(mNumAudioSources);
              dest.writeInt(mIsInitializationRequired ? 1 : 0);
              dest.writeInt(mIsCaptureSupported ? 1 : 0);
              dest.writeParcelableArray(mBands, flags);
              dest.writeInt(mIsBgScanSupported ? 1 : 0);
              dest.writeIntArray(setToArray(mSupportedProgramTypes));
              dest.writeIntArray(setToArray(mSupportedIdentifierTypes));
              Utils.writeStringIntMap(dest, mDabFrequencyTable);
              Utils.writeStringMap(dest, mVendorInfo);
          }
  
          @Override
          public int describeContents() {
              return 0;
          }
  
          @Override
          public String toString() {
              return "ModuleProperties [mId=" + mId
                      + ", mServiceName=" + mServiceName + ", mClassId=" + mClassId
                      + ", mImplementor=" + mImplementor + ", mProduct=" + mProduct
                      + ", mVersion=" + mVersion + ", mSerial=" + mSerial
                      + ", mNumTuners=" + mNumTuners
                      + ", mNumAudioSources=" + mNumAudioSources
                      + ", mIsInitializationRequired=" + mIsInitializationRequired
                      + ", mIsCaptureSupported=" + mIsCaptureSupported
                      + ", mIsBgScanSupported=" + mIsBgScanSupported
                      + ", mBands=" + Arrays.toString(mBands) + "]";
          }
  
          @Override
          public int hashCode() {
              return Objects.hash(mId, mServiceName, mClassId, mImplementor, mProduct, mVersion,
                  mSerial, mNumTuners, mNumAudioSources, mIsInitializationRequired,
                  mIsCaptureSupported, mBands, mIsBgScanSupported, mDabFrequencyTable, mVendorInfo);
          }
  
          @Override
          public boolean equals(Object obj) {
              if (this == obj) return true;
              if (!(obj instanceof ModuleProperties)) return false;
              ModuleProperties other = (ModuleProperties) obj;
  
              if (mId != other.getId()) return false;
              if (!TextUtils.equals(mServiceName, other.mServiceName)) return false;
              if (mClassId != other.mClassId) return false;
              if (!Objects.equals(mImplementor, other.mImplementor)) return false;
              if (!Objects.equals(mProduct, other.mProduct)) return false;
              if (!Objects.equals(mVersion, other.mVersion)) return false;
              if (!Objects.equals(mSerial, other.mSerial)) return false;
              if (mNumTuners != other.mNumTuners) return false;
              if (mNumAudioSources != other.mNumAudioSources) return false;
              if (mIsInitializationRequired != other.mIsInitializationRequired) return false;
              if (mIsCaptureSupported != other.mIsCaptureSupported) return false;
              if (!Objects.equals(mBands, other.mBands)) return false;
              if (mIsBgScanSupported != other.mIsBgScanSupported) return false;
              if (!Objects.equals(mDabFrequencyTable, other.mDabFrequencyTable)) return false;
              if (!Objects.equals(mVendorInfo, other.mVendorInfo)) return false;
              return true;
          }
      }
  
      /** Radio band descriptor: an element in ModuleProperties bands array.
       * It is either an instance of {@link FmBandDescriptor} or {@link AmBandDescriptor} */
      public static class BandDescriptor implements Parcelable {
  
          private final int mRegion;
          private final int mType;
          private final int mLowerLimit;
          private final int mUpperLimit;
          private final int mSpacing;
  
          BandDescriptor(int region, int type, int lowerLimit, int upperLimit, int spacing) {
              if (type != BAND_AM && type != BAND_FM && type != BAND_FM_HD && type != BAND_AM_HD) {
                  throw new IllegalArgumentException("Unsupported band: " + type);
              }
              mRegion = region;
              mType = type;
              mLowerLimit = lowerLimit;
              mUpperLimit = upperLimit;
              mSpacing = spacing;
          }
  
          /** Region this band applies to. E.g. {@link #REGION_ITU_1}
           * @return the region this band is associated to.
           */
          public int getRegion() {
              return mRegion;
          }
          /** Band type, e.g {@link #BAND_FM}. Defines the subclass this descriptor can be cast to:
           * <ul>
           *  <li>{@link #BAND_FM} or {@link #BAND_FM_HD} cast to {@link FmBandDescriptor}, </li>
           *  <li>{@link #BAND_AM} cast to {@link AmBandDescriptor}, </li>
           * </ul>
           * @return the band type.
           */
          public int getType() {
              return mType;
          }
  
          /**
           * Checks if the band is either AM or AM_HD.
           *
           * @return {@code true}, if band is AM or AM_HD.
           */
          public boolean isAmBand() {
              return mType == BAND_AM || mType == BAND_AM_HD;
          }
  
          /**
           * Checks if the band is either FM or FM_HD.
           *
           * @return {@code true}, if band is FM or FM_HD.
           */
          public boolean isFmBand() {
              return mType == BAND_FM || mType == BAND_FM_HD;
          }
  
          /** Lower band limit expressed in units according to band type.
           * Currently all defined band types express channels as frequency in kHz
           * @return the lower band limit.
           */
          public int getLowerLimit() {
              return mLowerLimit;
          }
          /** Upper band limit expressed in units according to band type.
           * Currently all defined band types express channels as frequency in kHz
           * @return the upper band limit.
           */
          public int getUpperLimit() {
              return mUpperLimit;
          }
          /** Channel spacing in units according to band type.
           * Currently all defined band types express channels as frequency in kHz
           * @return the channel spacing.
           */
          public int getSpacing() {
              return mSpacing;
          }
  
          private BandDescriptor(Parcel in) {
              mRegion = in.readInt();
              mType = in.readInt();
              mLowerLimit = in.readInt();
              mUpperLimit = in.readInt();
              mSpacing = in.readInt();
          }
  
          private static int lookupTypeFromParcel(Parcel in) {
              int pos = in.dataPosition();
              in.readInt();  // skip region
              int type = in.readInt();
              in.setDataPosition(pos);
              return type;
          }
  
          public static final @android.annotation.NonNull Parcelable.Creator<BandDescriptor> CREATOR
                  = new Parcelable.Creator<BandDescriptor>() {
              public BandDescriptor createFromParcel(Parcel in) {
                  int type = lookupTypeFromParcel(in);
                  switch (type) {
                      case BAND_FM:
                      case BAND_FM_HD:
                          return new FmBandDescriptor(in);
                      case BAND_AM:
                      case BAND_AM_HD:
                          return new AmBandDescriptor(in);
                      default:
                          throw new IllegalArgumentException("Unsupported band: " + type);
                  }
              }
  
              public BandDescriptor[] newArray(int size) {
                  return new BandDescriptor[size];
              }
          };
  
          @Override
          public void writeToParcel(Parcel dest, int flags) {
              dest.writeInt(mRegion);
              dest.writeInt(mType);
              dest.writeInt(mLowerLimit);
              dest.writeInt(mUpperLimit);
              dest.writeInt(mSpacing);
          }
  
          @Override
          public int describeContents() {
              return 0;
          }
  
          @Override
          public String toString() {
              return "BandDescriptor [mRegion=" + mRegion + ", mType=" + mType + ", mLowerLimit="
                      + mLowerLimit + ", mUpperLimit=" + mUpperLimit + ", mSpacing=" + mSpacing + "]";
          }
  
          @Override
          public int hashCode() {
              final int prime = 31;
              int result = 1;
              result = prime * result + mRegion;
              result = prime * result + mType;
              result = prime * result + mLowerLimit;
              result = prime * result + mUpperLimit;
              result = prime * result + mSpacing;
              return result;
          }
  
          @Override
          public boolean equals(Object obj) {
              if (this == obj)
                  return true;
              if (!(obj instanceof BandDescriptor))
                  return false;
              BandDescriptor other = (BandDescriptor) obj;
              if (mRegion != other.getRegion())
                  return false;
              if (mType != other.getType())
                  return false;
              if (mLowerLimit != other.getLowerLimit())
                  return false;
              if (mUpperLimit != other.getUpperLimit())
                  return false;
              if (mSpacing != other.getSpacing())
                  return false;
              return true;
          }
      }
  
      /** FM band descriptor
       * @see #BAND_FM
       * @see #BAND_FM_HD */
      public static class FmBandDescriptor extends BandDescriptor {
          private final boolean mStereo;
          private final boolean mRds;
          private final boolean mTa;
          private final boolean mAf;
          private final boolean mEa;
  
          /** @hide */
          public FmBandDescriptor(int region, int type, int lowerLimit, int upperLimit, int spacing,
                  boolean stereo, boolean rds, boolean ta, boolean af, boolean ea) {
              super(region, type, lowerLimit, upperLimit, spacing);
              mStereo = stereo;
              mRds = rds;
              mTa = ta;
              mAf = af;
              mEa = ea;
          }
  
          /** Stereo is supported
           * @return {@code true} if stereo is supported, {@code false} otherwise.
           */
          public boolean isStereoSupported() {
              return mStereo;
          }
          /** RDS or RBDS(if region is ITU2) is supported
           * @return {@code true} if RDS or RBDS is supported, {@code false} otherwise.
           */
          public boolean isRdsSupported() {
              return mRds;
          }
          /** Traffic announcement is supported
           * @return {@code true} if TA is supported, {@code false} otherwise.
           */
          public boolean isTaSupported() {
              return mTa;
          }
          /** Alternate Frequency Switching is supported
           * @return {@code true} if AF switching is supported, {@code false} otherwise.
           */
          public boolean isAfSupported() {
              return mAf;
          }
  
          /** Emergency Announcement is supported
           * @return {@code true} if Emergency annoucement is supported, {@code false} otherwise.
           */
          public boolean isEaSupported() {
              return mEa;
          }
  
          /* Parcelable implementation */
          private FmBandDescriptor(Parcel in) {
              super(in);
              mStereo = in.readByte() == 1;
              mRds = in.readByte() == 1;
              mTa = in.readByte() == 1;
              mAf = in.readByte() == 1;
              mEa = in.readByte() == 1;
          }
  
          public static final @android.annotation.NonNull Parcelable.Creator<FmBandDescriptor> CREATOR
                  = new Parcelable.Creator<FmBandDescriptor>() {
              public FmBandDescriptor createFromParcel(Parcel in) {
                  return new FmBandDescriptor(in);
              }
  
              public FmBandDescriptor[] newArray(int size) {
                  return new FmBandDescriptor[size];
              }
          };
  
          @Override
          public void writeToParcel(Parcel dest, int flags) {
              super.writeToParcel(dest, flags);
              dest.writeByte((byte) (mStereo ? 1 : 0));
              dest.writeByte((byte) (mRds ? 1 : 0));
              dest.writeByte((byte) (mTa ? 1 : 0));
              dest.writeByte((byte) (mAf ? 1 : 0));
              dest.writeByte((byte) (mEa ? 1 : 0));
          }
  
          @Override
          public int describeContents() {
              return 0;
          }
  
          @Override
          public String toString() {
              return "FmBandDescriptor [ "+ super.toString() + " mStereo=" + mStereo
                      + ", mRds=" + mRds + ", mTa=" + mTa + ", mAf=" + mAf +
                      ", mEa =" + mEa + "]";
          }
  
          @Override
          public int hashCode() {
              final int prime = 31;
              int result = super.hashCode();
              result = prime * result + (mStereo ? 1 : 0);
              result = prime * result + (mRds ? 1 : 0);
              result = prime * result + (mTa ? 1 : 0);
              result = prime * result + (mAf ? 1 : 0);
              result = prime * result + (mEa ? 1 : 0);
              return result;
          }
  
          @Override
          public boolean equals(Object obj) {
              if (this == obj)
                  return true;
              if (!super.equals(obj))
                  return false;
              if (!(obj instanceof FmBandDescriptor))
                  return false;
              FmBandDescriptor other = (FmBandDescriptor) obj;
              if (mStereo != other.isStereoSupported())
                  return false;
              if (mRds != other.isRdsSupported())
                  return false;
              if (mTa != other.isTaSupported())
                  return false;
              if (mAf != other.isAfSupported())
                  return false;
              if (mEa != other.isEaSupported())
                  return false;
              return true;
          }
      }
  
      /** AM band descriptor.
       * @see #BAND_AM */
      public static class AmBandDescriptor extends BandDescriptor {
  
          private final boolean mStereo;
  
          /** @hide */
          public AmBandDescriptor(int region, int type, int lowerLimit, int upperLimit, int spacing,
                  boolean stereo) {
              super(region, type, lowerLimit, upperLimit, spacing);
              mStereo = stereo;
          }
  
          /** Stereo is supported
           *  @return {@code true} if stereo is supported, {@code false} otherwise.
           */
          public boolean isStereoSupported() {
              return mStereo;
          }
  
          private AmBandDescriptor(Parcel in) {
              super(in);
              mStereo = in.readByte() == 1;
          }
  
          public static final @android.annotation.NonNull Parcelable.Creator<AmBandDescriptor> CREATOR
                  = new Parcelable.Creator<AmBandDescriptor>() {
              public AmBandDescriptor createFromParcel(Parcel in) {
                  return new AmBandDescriptor(in);
              }
  
              public AmBandDescriptor[] newArray(int size) {
                  return new AmBandDescriptor[size];
              }
          };
  
          @Override
          public void writeToParcel(Parcel dest, int flags) {
              super.writeToParcel(dest, flags);
              dest.writeByte((byte) (mStereo ? 1 : 0));
          }
  
          @Override
          public int describeContents() {
              return 0;
          }
  
          @Override
          public String toString() {
              return "AmBandDescriptor [ "+ super.toString() + " mStereo=" + mStereo + "]";
          }
  
          @Override
          public int hashCode() {
              final int prime = 31;
              int result = super.hashCode();
              result = prime * result + (mStereo ? 1 : 0);
              return result;
          }
  
          @Override
          public boolean equals(Object obj) {
              if (this == obj)
                  return true;
              if (!super.equals(obj))
                  return false;
              if (!(obj instanceof AmBandDescriptor))
                  return false;
              AmBandDescriptor other = (AmBandDescriptor) obj;
              if (mStereo != other.isStereoSupported())
                  return false;
              return true;
          }
      }
  
  
      /** Radio band configuration. */
      public static class BandConfig implements Parcelable {
  
          @NonNull final BandDescriptor mDescriptor;
  
          BandConfig(BandDescriptor descriptor) {
              mDescriptor = Objects.requireNonNull(descriptor);
          }
  
          BandConfig(int region, int type, int lowerLimit, int upperLimit, int spacing) {
              mDescriptor = new BandDescriptor(region, type, lowerLimit, upperLimit, spacing);
          }
  
          private BandConfig(Parcel in) {
              mDescriptor = new BandDescriptor(in);
          }
  
          BandDescriptor getDescriptor() {
              return mDescriptor;
          }
  
          /** Region this band applies to. E.g. {@link #REGION_ITU_1}
           *  @return the region associated with this band.
           */
          public int getRegion() {
              return mDescriptor.getRegion();
          }
          /** Band type, e.g {@link #BAND_FM}. Defines the subclass this descriptor can be cast to:
           * <ul>
           *  <li>{@link #BAND_FM} or {@link #BAND_FM_HD} cast to {@link FmBandDescriptor}, </li>
           *  <li>{@link #BAND_AM} cast to {@link AmBandDescriptor}, </li>
           * </ul>
           *  @return the band type.
           */
          public int getType() {
              return mDescriptor.getType();
          }
          /** Lower band limit expressed in units according to band type.
           * Currently all defined band types express channels as frequency in kHz
           *  @return the lower band limit.
           */
          public int getLowerLimit() {
              return mDescriptor.getLowerLimit();
          }
          /** Upper band limit expressed in units according to band type.
           * Currently all defined band types express channels as frequency in kHz
           *  @return the upper band limit.
           */
          public int getUpperLimit() {
              return mDescriptor.getUpperLimit();
          }
          /** Channel spacing in units according to band type.
           * Currently all defined band types express channels as frequency in kHz
           *  @return the channel spacing.
           */
          public int getSpacing() {
              return mDescriptor.getSpacing();
          }
  
  
          public static final @android.annotation.NonNull Parcelable.Creator<BandConfig> CREATOR
                  = new Parcelable.Creator<BandConfig>() {
              public BandConfig createFromParcel(Parcel in) {
                  int type = BandDescriptor.lookupTypeFromParcel(in);
                  switch (type) {
                      case BAND_FM:
                      case BAND_FM_HD:
                          return new FmBandConfig(in);
                      case BAND_AM:
                      case BAND_AM_HD:
                          return new AmBandConfig(in);
                      default:
                          throw new IllegalArgumentException("Unsupported band: " + type);
                  }
              }
  
              public BandConfig[] newArray(int size) {
                  return new BandConfig[size];
              }
          };
  
          @Override
          public void writeToParcel(Parcel dest, int flags) {
              mDescriptor.writeToParcel(dest, flags);
          }
  
          @Override
          public int describeContents() {
              return 0;
          }
  
          @Override
          public String toString() {
              return "BandConfig [ " + mDescriptor.toString() + "]";
          }
  
          @Override
          public int hashCode() {
              final int prime = 31;
              int result = 1;
              result = prime * result + mDescriptor.hashCode();
              return result;
          }
  
          @Override
          public boolean equals(Object obj) {
              if (this == obj)
                  return true;
              if (!(obj instanceof BandConfig))
                  return false;
              BandConfig other = (BandConfig) obj;
              BandDescriptor otherDesc = other.getDescriptor();
              if ((mDescriptor == null) != (otherDesc == null)) return false;
              if (mDescriptor != null && !mDescriptor.equals(otherDesc)) return false;
              return true;
          }
      }
  
      /** FM band configuration.
       * @see #BAND_FM
       * @see #BAND_FM_HD */
      public static class FmBandConfig extends BandConfig {
          private final boolean mStereo;
          private final boolean mRds;
          private final boolean mTa;
          private final boolean mAf;
          private final boolean mEa;
  
          /** @hide */
          public FmBandConfig(FmBandDescriptor descriptor) {
              super((BandDescriptor)descriptor);
              mStereo = descriptor.isStereoSupported();
              mRds = descriptor.isRdsSupported();
              mTa = descriptor.isTaSupported();
              mAf = descriptor.isAfSupported();
              mEa = descriptor.isEaSupported();
          }
  
          FmBandConfig(int region, int type, int lowerLimit, int upperLimit, int spacing,
                  boolean stereo, boolean rds, boolean ta, boolean af, boolean ea) {
              super(region, type, lowerLimit, upperLimit, spacing);
              mStereo = stereo;
              mRds = rds;
              mTa = ta;
              mAf = af;
              mEa = ea;
          }
  
          /** Get stereo enable state
           * @return the enable state.
           */
          public boolean getStereo() {
              return mStereo;
          }
  
          /** Get RDS or RBDS(if region is ITU2) enable state
           * @return the enable state.
           */
          public boolean getRds() {
              return mRds;
          }
  
          /** Get Traffic announcement enable state
           * @return the enable state.
           */
          public boolean getTa() {
              return mTa;
          }
  
          /** Get Alternate Frequency Switching enable state
           * @return the enable state.
           */
          public boolean getAf() {
              return mAf;
          }
  
          /**
           * Get Emergency announcement enable state
           * @return the enable state.
           */
          public boolean getEa() {
              return mEa;
          }
  
          private FmBandConfig(Parcel in) {
              super(in);
              mStereo = in.readByte() == 1;
              mRds = in.readByte() == 1;
              mTa = in.readByte() == 1;
              mAf = in.readByte() == 1;
              mEa = in.readByte() == 1;
          }
  
          public static final @android.annotation.NonNull Parcelable.Creator<FmBandConfig> CREATOR
                  = new Parcelable.Creator<FmBandConfig>() {
              public FmBandConfig createFromParcel(Parcel in) {
                  return new FmBandConfig(in);
              }
  
              public FmBandConfig[] newArray(int size) {
                  return new FmBandConfig[size];
              }
          };
  
          @Override
          public void writeToParcel(Parcel dest, int flags) {
              super.writeToParcel(dest, flags);
              dest.writeByte((byte) (mStereo ? 1 : 0));
              dest.writeByte((byte) (mRds ? 1 : 0));
              dest.writeByte((byte) (mTa ? 1 : 0));
              dest.writeByte((byte) (mAf ? 1 : 0));
              dest.writeByte((byte) (mEa ? 1 : 0));
          }
  
          @Override
          public int describeContents() {
              return 0;
          }
  
          @Override
          public String toString() {
              return "FmBandConfig [" + super.toString()
                      + ", mStereo=" + mStereo + ", mRds=" + mRds + ", mTa=" + mTa
                      + ", mAf=" + mAf + ", mEa =" + mEa + "]";
          }
  
          @Override
          public int hashCode() {
              final int prime = 31;
              int result = super.hashCode();
              result = prime * result + (mStereo ? 1 : 0);
              result = prime * result + (mRds ? 1 : 0);
              result = prime * result + (mTa ? 1 : 0);
              result = prime * result + (mAf ? 1 : 0);
              result = prime * result + (mEa ? 1 : 0);
              return result;
          }
  
          @Override
          public boolean equals(Object obj) {
              if (this == obj)
                  return true;
              if (!super.equals(obj))
                  return false;
              if (!(obj instanceof FmBandConfig))
                  return false;
              FmBandConfig other = (FmBandConfig) obj;
              if (mStereo != other.mStereo)
                  return false;
              if (mRds != other.mRds)
                  return false;
              if (mTa != other.mTa)
                  return false;
              if (mAf != other.mAf)
                  return false;
              if (mEa != other.mEa)
                  return false;
              return true;
          }
  
          /**
           * Builder class for {@link FmBandConfig} objects.
           */
          public static class Builder {
              private final BandDescriptor mDescriptor;
              private boolean mStereo;
              private boolean mRds;
              private boolean mTa;
              private boolean mAf;
              private boolean mEa;
  
              /**
               * Constructs a new Builder with the defaults from an {@link FmBandDescriptor} .
               * @param descriptor the FmBandDescriptor defaults are read from .
               */
              public Builder(FmBandDescriptor descriptor) {
                  mDescriptor = new BandDescriptor(descriptor.getRegion(), descriptor.getType(),
                          descriptor.getLowerLimit(), descriptor.getUpperLimit(),
                          descriptor.getSpacing());
                  mStereo = descriptor.isStereoSupported();
                  mRds = descriptor.isRdsSupported();
                  mTa = descriptor.isTaSupported();
                  mAf = descriptor.isAfSupported();
                  mEa = descriptor.isEaSupported();
              }
  
              /**
               * Constructs a new Builder from a given {@link FmBandConfig}
               * @param config the FmBandConfig object whose data will be reused in the new Builder.
               */
              public Builder(FmBandConfig config) {
                  mDescriptor = new BandDescriptor(config.getRegion(), config.getType(),
                          config.getLowerLimit(), config.getUpperLimit(), config.getSpacing());
                  mStereo = config.getStereo();
                  mRds = config.getRds();
                  mTa = config.getTa();
                  mAf = config.getAf();
                  mEa = config.getEa();
              }
  
              /**
               * Combines all of the parameters that have been set and return a new
               * {@link FmBandConfig} object.
               * @return a new {@link FmBandConfig} object
               */
              public FmBandConfig build() {
                  FmBandConfig config = new FmBandConfig(mDescriptor.getRegion(),
                          mDescriptor.getType(), mDescriptor.getLowerLimit(),
                          mDescriptor.getUpperLimit(), mDescriptor.getSpacing(),
                          mStereo, mRds, mTa, mAf, mEa);
                  return config;
              }
  
              /** Set stereo enable state
               * @param state The new enable state.
               * @return the same Builder instance.
               */
              public Builder setStereo(boolean state) {
                  mStereo = state;
                  return this;
              }
  
              /** Set RDS or RBDS(if region is ITU2) enable state
               * @param state The new enable state.
               * @return the same Builder instance.
               */
              public Builder setRds(boolean state) {
                  mRds = state;
                  return this;
              }
  
              /** Set Traffic announcement enable state
               * @param state The new enable state.
               * @return the same Builder instance.
               */
              public Builder setTa(boolean state) {
                  mTa = state;
                  return this;
              }
  
              /** Set Alternate Frequency Switching enable state
               * @param state The new enable state.
               * @return the same Builder instance.
               */
              public Builder setAf(boolean state) {
                  mAf = state;
                  return this;
              }
  
              /** Set Emergency Announcement enable state
               * @param state The new enable state.
               * @return the same Builder instance.
               */
              public Builder setEa(boolean state) {
                  mEa = state;
                  return this;
              }
          };
      }
  
      /** AM band configuration.
       * @see #BAND_AM */
      public static class AmBandConfig extends BandConfig {
          private final boolean mStereo;
  
          /** @hide */
          public AmBandConfig(AmBandDescriptor descriptor) {
              super((BandDescriptor)descriptor);
              mStereo = descriptor.isStereoSupported();
          }
  
          AmBandConfig(int region, int type, int lowerLimit, int upperLimit, int spacing,
                  boolean stereo) {
              super(region, type, lowerLimit, upperLimit, spacing);
              mStereo = stereo;
          }
  
          /** Get stereo enable state
           * @return the enable state.
           */
          public boolean getStereo() {
              return mStereo;
          }
  
          private AmBandConfig(Parcel in) {
              super(in);
              mStereo = in.readByte() == 1;
          }
  
          public static final @android.annotation.NonNull Parcelable.Creator<AmBandConfig> CREATOR
                  = new Parcelable.Creator<AmBandConfig>() {
              public AmBandConfig createFromParcel(Parcel in) {
                  return new AmBandConfig(in);
              }
  
              public AmBandConfig[] newArray(int size) {
                  return new AmBandConfig[size];
              }
          };
  
          @Override
          public void writeToParcel(Parcel dest, int flags) {
              super.writeToParcel(dest, flags);
              dest.writeByte((byte) (mStereo ? 1 : 0));
          }
  
          @Override
          public int describeContents() {
              return 0;
          }
  
          @Override
          public String toString() {
              return "AmBandConfig [" + super.toString()
                      + ", mStereo=" + mStereo + "]";
          }
  
          @Override
          public int hashCode() {
              final int prime = 31;
              int result = super.hashCode();
              result = prime * result + (mStereo ? 1 : 0);
              return result;
          }
  
          @Override
          public boolean equals(Object obj) {
              if (this == obj)
                  return true;
              if (!super.equals(obj))
                  return false;
              if (!(obj instanceof AmBandConfig))
                  return false;
              AmBandConfig other = (AmBandConfig) obj;
              if (mStereo != other.getStereo())
                  return false;
              return true;
          }
  
          /**
           * Builder class for {@link AmBandConfig} objects.
           */
          public static class Builder {
              private final BandDescriptor mDescriptor;
              private boolean mStereo;
  
              /**
               * Constructs a new Builder with the defaults from an {@link AmBandDescriptor} .
               * @param descriptor the FmBandDescriptor defaults are read from .
               */
              public Builder(AmBandDescriptor descriptor) {
                  mDescriptor = new BandDescriptor(descriptor.getRegion(), descriptor.getType(),
                          descriptor.getLowerLimit(), descriptor.getUpperLimit(),
                          descriptor.getSpacing());
                  mStereo = descriptor.isStereoSupported();
              }
  
              /**
               * Constructs a new Builder from a given {@link AmBandConfig}
               * @param config the FmBandConfig object whose data will be reused in the new Builder.
               */
              public Builder(AmBandConfig config) {
                  mDescriptor = new BandDescriptor(config.getRegion(), config.getType(),
                          config.getLowerLimit(), config.getUpperLimit(), config.getSpacing());
                  mStereo = config.getStereo();
              }
  
              /**
               * Combines all of the parameters that have been set and return a new
               * {@link AmBandConfig} object.
               * @return a new {@link AmBandConfig} object
               */
              public AmBandConfig build() {
                  AmBandConfig config = new AmBandConfig(mDescriptor.getRegion(),
                          mDescriptor.getType(), mDescriptor.getLowerLimit(),
                          mDescriptor.getUpperLimit(), mDescriptor.getSpacing(),
                          mStereo);
                  return config;
              }
  
              /** Set stereo enable state
               * @param state The new enable state.
               * @return the same Builder instance.
               */
              public Builder setStereo(boolean state) {
                  mStereo = state;
                  return this;
              }
          };
      }
  
      /** Radio program information. */
      public static class ProgramInfo implements Parcelable {
  
          // sourced from hardware/interfaces/broadcastradio/2.0/types.hal
          private static final int FLAG_LIVE = 1 << 0;
          private static final int FLAG_MUTED = 1 << 1;
          private static final int FLAG_TRAFFIC_PROGRAM = 1 << 2;
          private static final int FLAG_TRAFFIC_ANNOUNCEMENT = 1 << 3;
          private static final int FLAG_TUNED = 1 << 4;
          private static final int FLAG_STEREO = 1 << 5;
  
          @NonNull private final ProgramSelector mSelector;
          @Nullable private final ProgramSelector.Identifier mLogicallyTunedTo;
          @Nullable private final ProgramSelector.Identifier mPhysicallyTunedTo;
          @NonNull private final Collection<ProgramSelector.Identifier> mRelatedContent;
          private final int mInfoFlags;
          private final int mSignalQuality;
          @Nullable private final RadioMetadata mMetadata;
          @NonNull private final Map<String, String> mVendorInfo;
  
          /** @hide */
          public ProgramInfo(@NonNull ProgramSelector selector,
                  @Nullable ProgramSelector.Identifier logicallyTunedTo,
                  @Nullable ProgramSelector.Identifier physicallyTunedTo,
                  @Nullable Collection<ProgramSelector.Identifier> relatedContent,
                  int infoFlags, int signalQuality, @Nullable RadioMetadata metadata,
                  @Nullable Map<String, String> vendorInfo) {
              mSelector = Objects.requireNonNull(selector);
              mLogicallyTunedTo = logicallyTunedTo;
              mPhysicallyTunedTo = physicallyTunedTo;
              if (relatedContent == null) {
                  mRelatedContent = Collections.emptyList();
              } else {
                  Preconditions.checkCollectionElementsNotNull(relatedContent, "relatedContent");
                  mRelatedContent = relatedContent;
              }
              mInfoFlags = infoFlags;
              mSignalQuality = signalQuality;
              mMetadata = metadata;
              mVendorInfo = (vendorInfo == null) ? new HashMap<>() : vendorInfo;
          }
  
          /**
           * Program selector, necessary for tuning to a program.
           *
           * @return the program selector.
           */
          public @NonNull ProgramSelector getSelector() {
              return mSelector;
          }
  
          /**
           * Identifier currently used for program selection.
           *
           * This identifier can be used to determine which technology is
           * currently being used for reception.
           *
           * Some program selectors contain tuning information for different radio
           * technologies (i.e. FM RDS and DAB). For example, user may tune using
           * a ProgramSelector with RDS_PI primary identifier, but the tuner hardware
           * may choose to use DAB technology to make actual tuning. This identifier
           * must reflect that.
           */
          public @Nullable ProgramSelector.Identifier getLogicallyTunedTo() {
              return mLogicallyTunedTo;
          }
  
          /**
           * Identifier currently used by hardware to physically tune to a channel.
           *
           * Some radio technologies broadcast the same program on multiple channels,
           * i.e. with RDS AF the same program may be broadcasted on multiple
           * alternative frequencies; the same DAB program may be broadcast on
           * multiple ensembles. This identifier points to the channel to which the
           * radio hardware is physically tuned to.
           */
          public @Nullable ProgramSelector.Identifier getPhysicallyTunedTo() {
              return mPhysicallyTunedTo;
          }
  
          /**
           * Primary identifiers of related contents.
           *
           * Some radio technologies provide pointers to other programs that carry
           * related content (i.e. DAB soft-links). This field is a list of pointers
           * to other programs on the program list.
           *
           * Please note, that these identifiers does not have to exist on the program
           * list - i.e. DAB tuner may provide information on FM RDS alternatives
           * despite not supporting FM RDS. If the system has multiple tuners, another
           * one may have it on its list.
           */
          public @Nullable Collection<ProgramSelector.Identifier> getRelatedContent() {
              return mRelatedContent;
          }
  
          /** Main channel expressed in units according to band type.
           * Currently all defined band types express channels as frequency in kHz
           * @return the program channel
           * @deprecated Use {@link getSelector()} instead.
           */
          @Deprecated
          public int getChannel() {
              try {
                  return (int) mSelector.getFirstId(ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY);
              } catch (IllegalArgumentException ex) {
                  Log.w(TAG, "Not an AM/FM program");
                  return 0;
              }
          }
  
          /** Sub channel ID. E.g 1 for HD radio HD1
           * @return the program sub channel
           * @deprecated Use {@link getSelector()} instead.
           */
          @Deprecated
          public int getSubChannel() {
              try {
                  return (int) mSelector.getFirstId(
                          ProgramSelector.IDENTIFIER_TYPE_HD_SUBCHANNEL) + 1;
              } catch (IllegalArgumentException ex) {
                  // this is a normal behavior for analog AM/FM selector
                  return 0;
              }
          }
  
          /** {@code true} if the tuner is currently tuned on a valid station
           * @return {@code true} if currently tuned, {@code false} otherwise.
           */
          public boolean isTuned() {
              return (mInfoFlags & FLAG_TUNED) != 0;
          }
  
          /** {@code true} if the received program is stereo
           * @return {@code true} if stereo, {@code false} otherwise.
           */
          public boolean isStereo() {
              return (mInfoFlags & FLAG_STEREO) != 0;
          }
  
          /** {@code true} if the received program is digital (e.g HD radio)
           * @return {@code true} if digital, {@code false} otherwise.
           * @deprecated Use {@link getLogicallyTunedTo()} instead.
           */
          @Deprecated
          public boolean isDigital() {
              ProgramSelector.Identifier id = mLogicallyTunedTo;
              if (id == null) id = mSelector.getPrimaryId();
  
              int type = id.getType();
              return (type != ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY
                  && type != ProgramSelector.IDENTIFIER_TYPE_RDS_PI);
          }
  
          /**
           * {@code true} if the program is currently playing live stream.
           * This may result in a slightly altered reception parameters,
           * usually targetted at reduced latency.
           */
          public boolean isLive() {
              return (mInfoFlags & FLAG_LIVE) != 0;
          }
  
          /**
           * {@code true} if radio stream is not playing, ie. due to bad reception
           * conditions or buffering. In this state volume knob MAY be disabled to
           * prevent user increasing volume too much.
           * It does NOT mean the user has muted audio.
           */
          public boolean isMuted() {
              return (mInfoFlags & FLAG_MUTED) != 0;
          }
  
          /**
           * {@code true} if radio station transmits traffic information
           * regularily.
           */
          public boolean isTrafficProgram() {
              return (mInfoFlags & FLAG_TRAFFIC_PROGRAM) != 0;
          }
  
          /**
           * {@code true} if radio station transmits traffic information
           * at the very moment.
           */
          public boolean isTrafficAnnouncementActive() {
              return (mInfoFlags & FLAG_TRAFFIC_ANNOUNCEMENT) != 0;
          }
  
          /**
           * Signal quality (as opposed to the name) indication from 0 (no signal)
           * to 100 (excellent)
           * @return the signal quality indication.
           */
          public int getSignalStrength() {
              return mSignalQuality;
          }
  
          /** Metadata currently received from this station.
           * null if no metadata have been received
           * @return current meta data received from this program.
           */
          public RadioMetadata getMetadata() {
              return mMetadata;
          }
  
          /**
           * A map of vendor-specific opaque strings, passed from HAL without changes.
           * Format of these strings can vary across vendors.
           *
           * It may be used for extra features, that's not supported by a platform,
           * for example: paid-service=true; bitrate=320kbps.
           *
           * Keys must be prefixed with unique vendor Java-style namespace,
           * eg. 'com.somecompany.parameter1'.
           */
          public @NonNull Map<String, String> getVendorInfo() {
              return mVendorInfo;
          }
  
          private ProgramInfo(Parcel in) {
              mSelector = Objects.requireNonNull(in.readTypedObject(ProgramSelector.CREATOR));
              mLogicallyTunedTo = in.readTypedObject(ProgramSelector.Identifier.CREATOR);
              mPhysicallyTunedTo = in.readTypedObject(ProgramSelector.Identifier.CREATOR);
              mRelatedContent = in.createTypedArrayList(ProgramSelector.Identifier.CREATOR);
              mInfoFlags = in.readInt();
              mSignalQuality = in.readInt();
              mMetadata = in.readTypedObject(RadioMetadata.CREATOR);
              mVendorInfo = Utils.readStringMap(in);
          }
  
          public static final @android.annotation.NonNull Parcelable.Creator<ProgramInfo> CREATOR
                  = new Parcelable.Creator<ProgramInfo>() {
              public ProgramInfo createFromParcel(Parcel in) {
                  return new ProgramInfo(in);
              }
  
              public ProgramInfo[] newArray(int size) {
                  return new ProgramInfo[size];
              }
          };
  
          @Override
          public void writeToParcel(Parcel dest, int flags) {
              dest.writeTypedObject(mSelector, flags);
              dest.writeTypedObject(mLogicallyTunedTo, flags);
              dest.writeTypedObject(mPhysicallyTunedTo, flags);
              Utils.writeTypedCollection(dest, mRelatedContent);
              dest.writeInt(mInfoFlags);
              dest.writeInt(mSignalQuality);
              dest.writeTypedObject(mMetadata, flags);
              Utils.writeStringMap(dest, mVendorInfo);
          }
  
          @Override
          public int describeContents() {
              return 0;
          }
  
          @Override
          public String toString() {
              return "ProgramInfo"
                      + " [selector=" + mSelector
                      + ", logicallyTunedTo=" + Objects.toString(mLogicallyTunedTo)
                      + ", physicallyTunedTo=" + Objects.toString(mPhysicallyTunedTo)
                      + ", relatedContent=" + mRelatedContent.size()
                      + ", infoFlags=" + mInfoFlags
                      + ", mSignalQuality=" + mSignalQuality
                      + ", mMetadata=" + Objects.toString(mMetadata)
                      + "]";
          }
  
          @Override
          public int hashCode() {
              return Objects.hash(mSelector, mLogicallyTunedTo, mPhysicallyTunedTo,
                  mRelatedContent, mInfoFlags, mSignalQuality, mMetadata, mVendorInfo);
          }
  
          @Override
          public boolean equals(Object obj) {
              if (this == obj) return true;
              if (!(obj instanceof ProgramInfo)) return false;
              ProgramInfo other = (ProgramInfo) obj;
  
              if (!Objects.equals(mSelector, other.mSelector)) return false;
              if (!Objects.equals(mLogicallyTunedTo, other.mLogicallyTunedTo)) return false;
              if (!Objects.equals(mPhysicallyTunedTo, other.mPhysicallyTunedTo)) return false;
              if (!Objects.equals(mRelatedContent, other.mRelatedContent)) return false;
              if (mInfoFlags != other.mInfoFlags) return false;
              if (mSignalQuality != other.mSignalQuality) return false;
              if (!Objects.equals(mMetadata, other.mMetadata)) return false;
              if (!Objects.equals(mVendorInfo, other.mVendorInfo)) return false;
  
              return true;
          }
      }
  
  
      /**
       * Returns a list of descriptors for all broadcast radio modules present on the device.
       * @param modules An List of {@link ModuleProperties} where the list will be returned.
       * @return
       * <ul>
       *  <li>{@link #STATUS_OK} in case of success, </li>
       *  <li>{@link #STATUS_ERROR} in case of unspecified error, </li>
       *  <li>{@link #STATUS_NO_INIT} if the native service cannot be reached, </li>
       *  <li>{@link #STATUS_BAD_VALUE} if modules is null, </li>
       *  <li>{@link #STATUS_DEAD_OBJECT} if the binder transaction to the native service fails, </li>
       * </ul>
       */
      @RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO)
      public int listModules(List<ModuleProperties> modules) {
          if (modules == null) {
              Log.e(TAG, "the output list must not be empty");
              return STATUS_BAD_VALUE;
          }
  
          Log.d(TAG, "Listing available tuners...");
          List<ModuleProperties> returnedList;
          try {
              returnedList = mService.listModules();
          } catch (RemoteException e) {
              Log.e(TAG, "Failed listing available tuners", e);
              return STATUS_DEAD_OBJECT;
          }
  
          if (returnedList == null) {
              Log.e(TAG, "Returned list was a null");
              return STATUS_ERROR;
          }
  
          modules.addAll(returnedList);
          return STATUS_OK;
      }
  
      private native int nativeListModules(List<ModuleProperties> modules);
  
      /**
       * Open an interface to control a tuner on a given broadcast radio module.
       * Optionally selects and applies the configuration passed as "config" argument.
       * @param moduleId radio module identifier {@link ModuleProperties#getId()}. Mandatory.
       * @param config desired band and configuration to apply when enabling the hardware module.
       * optional, can be null.
       * @param withAudio {@code true} to request a tuner with an audio source.
       * This tuner is intended for live listening or recording or a radio program.
       * If {@code false}, the tuner can only be used to retrieve program informations.
       * @param callback {@link RadioTuner.Callback} interface. Mandatory.
       * @param handler the Handler on which the callbacks will be received.
       * Can be null if default handler is OK.
       * @return a valid {@link RadioTuner} interface in case of success or null in case of error.
       */
      @RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO)
      public RadioTuner openTuner(int moduleId, BandConfig config, boolean withAudio,
              RadioTuner.Callback callback, Handler handler) {
          if (callback == null) {
              throw new IllegalArgumentException("callback must not be empty");
          }
  
          Log.d(TAG, "Opening tuner " + moduleId + "...");
  
          ITuner tuner;
          TunerCallbackAdapter halCallback = new TunerCallbackAdapter(callback, handler);
          try {
              tuner = mService.openTuner(moduleId, config, withAudio, halCallback);
          } catch (RemoteException | IllegalArgumentException | IllegalStateException ex) {
              Log.e(TAG, "Failed to open tuner", ex);
              return null;
          }
          if (tuner == null) {
              Log.e(TAG, "Failed to open tuner");
              return null;
          }
          return new TunerAdapter(tuner, halCallback,
                  config != null ? config.getType() : BAND_INVALID);
      }
  
      private final Map<Announcement.OnListUpdatedListener, ICloseHandle> mAnnouncementListeners =
              new HashMap<>();
  
      /**
       * Adds new announcement listener.
       *
       * @param enabledAnnouncementTypes a set of announcement types to listen to
       * @param listener announcement listener
       */
      @RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO)
      public void addAnnouncementListener(@NonNull Set<Integer> enabledAnnouncementTypes,
              @NonNull Announcement.OnListUpdatedListener listener) {
          addAnnouncementListener(cmd -> cmd.run(), enabledAnnouncementTypes, listener);
      }
  
      /**
       * Adds new announcement listener with executor.
       *
       * @param executor the executor
       * @param enabledAnnouncementTypes a set of announcement types to listen to
       * @param listener announcement listener
       */
      @RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO)
      public void addAnnouncementListener(@NonNull @CallbackExecutor Executor executor,
              @NonNull Set<Integer> enabledAnnouncementTypes,
              @NonNull Announcement.OnListUpdatedListener listener) {
          Objects.requireNonNull(executor);
          Objects.requireNonNull(listener);
          int[] types = enabledAnnouncementTypes.stream().mapToInt(Integer::intValue).toArray();
          IAnnouncementListener listenerIface = new IAnnouncementListener.Stub() {
              public void onListUpdated(List<Announcement> activeAnnouncements) {
                  executor.execute(() -> listener.onListUpdated(activeAnnouncements));
              }
          };
          synchronized (mAnnouncementListeners) {
              ICloseHandle closeHandle = null;
              try {
                  closeHandle = mService.addAnnouncementListener(types, listenerIface);
              } catch (RemoteException ex) {
                  ex.rethrowFromSystemServer();
              }
              Objects.requireNonNull(closeHandle);
              ICloseHandle oldCloseHandle = mAnnouncementListeners.put(listener, closeHandle);
              if (oldCloseHandle != null) Utils.close(oldCloseHandle);
          }
      }
  
      /**
       * Removes previously registered announcement listener.
       *
       * @param listener announcement listener, previously registered with
       *        {@link addAnnouncementListener}
       */
      @RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO)
      public void removeAnnouncementListener(@NonNull Announcement.OnListUpdatedListener listener) {
          Objects.requireNonNull(listener);
          synchronized (mAnnouncementListeners) {
              ICloseHandle closeHandle = mAnnouncementListeners.remove(listener);
              if (closeHandle != null) Utils.close(closeHandle);
          }
      }
  
      @NonNull private final Context mContext;
      @NonNull private final IRadioService mService;
  
      /**
       * @hide
       */
      public RadioManager(@NonNull Context context) throws ServiceNotFoundException {
          mContext = context;
          mService = IRadioService.Stub.asInterface(
                  ServiceManager.getServiceOrThrow(Context.RADIO_SERVICE));
      }
  }
  