Merge "Implement ProgramSelector for font-end Java APIs."
diff --git a/Android.mk b/Android.mk
index 2d8739b..cca87ca 100644
--- a/Android.mk
+++ b/Android.mk
@@ -712,6 +712,7 @@
 	frameworks/base/core/java/android/print/PrinterInfo.aidl \
 	frameworks/base/core/java/android/print/PrintJobId.aidl \
 	frameworks/base/core/java/android/printservice/recommendation/RecommendationInfo.aidl \
+	frameworks/base/core/java/android/hardware/radio/ProgramSelector.aidl \
 	frameworks/base/core/java/android/hardware/radio/RadioManager.aidl \
 	frameworks/base/core/java/android/hardware/radio/RadioMetadata.aidl \
 	frameworks/base/core/java/android/hardware/usb/UsbDevice.aidl \
diff --git a/api/system-current.txt b/api/system-current.txt
index 678a4cd..d176e39 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -17215,6 +17215,63 @@
 
 package android.hardware.radio {
 
+  public final class ProgramSelector implements android.os.Parcelable {
+    ctor public ProgramSelector(int, android.hardware.radio.ProgramSelector.Identifier, android.hardware.radio.ProgramSelector.Identifier[], long[]);
+    method public static android.hardware.radio.ProgramSelector createAmFmSelector(int, int);
+    method public static android.hardware.radio.ProgramSelector createAmFmSelector(int, int, int);
+    method public int describeContents();
+    method public android.hardware.radio.ProgramSelector.Identifier[] getAllIds(int);
+    method public long getFirstId(int);
+    method public android.hardware.radio.ProgramSelector.Identifier getPrimaryId();
+    method public int getProgramType();
+    method public android.hardware.radio.ProgramSelector.Identifier[] getSecondaryIds();
+    method public long[] getVendorIds();
+    method public void writeToParcel(android.os.Parcel, int);
+    field public static final android.os.Parcelable.Creator<android.hardware.radio.ProgramSelector> CREATOR;
+    field public static final int IDENTIFIER_TYPE_AMFM_FREQUENCY = 1; // 0x1
+    field public static final int IDENTIFIER_TYPE_DAB_ENSEMBLE = 6; // 0x6
+    field public static final int IDENTIFIER_TYPE_DAB_FREQUENCY = 8; // 0x8
+    field public static final int IDENTIFIER_TYPE_DAB_SCID = 7; // 0x7
+    field public static final int IDENTIFIER_TYPE_DAB_SIDECC = 5; // 0x5
+    field public static final int IDENTIFIER_TYPE_DRMO_FREQUENCY = 10; // 0xa
+    field public static final int IDENTIFIER_TYPE_DRMO_SERVICE_ID = 9; // 0x9
+    field public static final int IDENTIFIER_TYPE_HD_STATION_ID_EXT = 3; // 0x3
+    field public static final int IDENTIFIER_TYPE_HD_SUBCHANNEL = 4; // 0x4
+    field public static final int IDENTIFIER_TYPE_RDS_PI = 2; // 0x2
+    field public static final int IDENTIFIER_TYPE_SXM_CHANNEL = 12; // 0xc
+    field public static final int IDENTIFIER_TYPE_SXM_SERVICE_ID = 11; // 0xb
+    field public static final int IDENTIFIER_TYPE_VENDOR1_PRIMARY = 13; // 0xd
+    field public static final int IDENTIFIER_TYPE_VENDOR2_PRIMARY = 14; // 0xe
+    field public static final int IDENTIFIER_TYPE_VENDOR3_PRIMARY = 15; // 0xf
+    field public static final int IDENTIFIER_TYPE_VENDOR4_PRIMARY = 16; // 0x10
+    field public static final int PROGRAM_TYPE_AM = 1; // 0x1
+    field public static final int PROGRAM_TYPE_AM_HD = 3; // 0x3
+    field public static final int PROGRAM_TYPE_DAB = 5; // 0x5
+    field public static final int PROGRAM_TYPE_DRMO = 6; // 0x6
+    field public static final int PROGRAM_TYPE_FM = 2; // 0x2
+    field public static final int PROGRAM_TYPE_FM_HD = 4; // 0x4
+    field public static final int PROGRAM_TYPE_SXM = 7; // 0x7
+    field public static final int PROGRAM_TYPE_VENDOR1 = 8; // 0x8
+    field public static final int PROGRAM_TYPE_VENDOR2 = 9; // 0x9
+    field public static final int PROGRAM_TYPE_VENDOR3 = 10; // 0xa
+    field public static final int PROGRAM_TYPE_VENDOR4 = 11; // 0xb
+  }
+
+  public static final class ProgramSelector.Identifier implements android.os.Parcelable {
+    ctor public ProgramSelector.Identifier(int, long);
+    method public int describeContents();
+    method public int getType();
+    method public long getValue();
+    method public void writeToParcel(android.os.Parcel, int);
+    field public static final android.os.Parcelable.Creator<android.hardware.radio.ProgramSelector.Identifier> CREATOR;
+  }
+
+  public static abstract class ProgramSelector.IdentifierType implements java.lang.annotation.Annotation {
+  }
+
+  public static abstract class ProgramSelector.ProgramType implements java.lang.annotation.Annotation {
+  }
+
   public class RadioManager {
     method public int listModules(java.util.List<android.hardware.radio.RadioManager.ModuleProperties>);
     method public android.hardware.radio.RadioTuner openTuner(int, android.hardware.radio.RadioManager.BandConfig, boolean, android.hardware.radio.RadioTuner.Callback, android.os.Handler);
@@ -17222,6 +17279,7 @@
     field public static final int BAND_AM_HD = 3; // 0x3
     field public static final int BAND_FM = 1; // 0x1
     field public static final int BAND_FM_HD = 2; // 0x2
+    field public static final int BAND_INVALID = -1; // 0xffffffff
     field public static final int CLASS_AM_FM = 0; // 0x0
     field public static final int CLASS_DT = 2; // 0x2
     field public static final int CLASS_SAT = 1; // 0x1
@@ -17257,6 +17315,9 @@
     field public static final android.os.Parcelable.Creator<android.hardware.radio.RadioManager.AmBandDescriptor> CREATOR;
   }
 
+  public static abstract class RadioManager.Band implements java.lang.annotation.Annotation {
+  }
+
   public static class RadioManager.BandConfig implements android.os.Parcelable {
     method public int describeContents();
     method public int getLowerLimit();
@@ -17331,10 +17392,11 @@
 
   public static class RadioManager.ProgramInfo implements android.os.Parcelable {
     method public int describeContents();
-    method public int getChannel();
+    method public deprecated int getChannel();
     method public android.hardware.radio.RadioMetadata getMetadata();
+    method public android.hardware.radio.ProgramSelector getSelector();
     method public int getSignalStrength();
-    method public int getSubChannel();
+    method public deprecated int getSubChannel();
     method public java.lang.String getVendorExension();
     method public boolean isDigital();
     method public boolean isLive();
@@ -17406,7 +17468,8 @@
     method public abstract int setMute(boolean);
     method public abstract boolean startBackgroundScan();
     method public abstract int step(int, boolean);
-    method public abstract int tune(int, int);
+    method public abstract deprecated int tune(int, int);
+    method public abstract void tune(android.hardware.radio.ProgramSelector);
     field public static final int DIRECTION_DOWN = 1; // 0x1
     field public static final int DIRECTION_UP = 0; // 0x0
     field public static final int ERROR_BACKGROUND_SCAN_FAILED = 6; // 0x6
diff --git a/core/java/android/hardware/radio/ITuner.aidl b/core/java/android/hardware/radio/ITuner.aidl
index 69e135d..2932c3e 100644
--- a/core/java/android/hardware/radio/ITuner.aidl
+++ b/core/java/android/hardware/radio/ITuner.aidl
@@ -16,6 +16,7 @@
 
 package android.hardware.radio;
 
+import android.hardware.radio.ProgramSelector;
 import android.hardware.radio.RadioManager;
 
 /** {@hide} */
@@ -52,7 +53,7 @@
      * @throws IllegalArgumentException if invalid arguments are passed
      * @throws IllegalStateException if called out of sequence
      */
-    void tune(int channel, int subChannel);
+    void tune(in ProgramSelector selector);
 
     /**
      * @throws IllegalStateException if called out of sequence
diff --git a/core/java/android/hardware/radio/ProgramSelector.aidl b/core/java/android/hardware/radio/ProgramSelector.aidl
new file mode 100644
index 0000000..545269a
--- /dev/null
+++ b/core/java/android/hardware/radio/ProgramSelector.aidl
@@ -0,0 +1,20 @@
+/**
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hardware.radio;
+
+/** @hide */
+parcelable ProgramSelector;
diff --git a/core/java/android/hardware/radio/ProgramSelector.java b/core/java/android/hardware/radio/ProgramSelector.java
new file mode 100644
index 0000000..4638dd5
--- /dev/null
+++ b/core/java/android/hardware/radio/ProgramSelector.java
@@ -0,0 +1,506 @@
+/**
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hardware.radio;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Stream;
+
+/**
+ * A set of identifiers necessary to tune to a given station.
+ *
+ * This can hold various identifiers, like
+ * - AM/FM frequency
+ * - HD Radio subchannel
+ * - DAB channel info
+ *
+ * The primary ID uniquely identifies a station and can be used for equality
+ * check. The secondary IDs are supplementary and can speed up tuning process,
+ * but the primary ID is sufficient (ie. after a full band scan).
+ *
+ * Two selectors with different secondary IDs, but the same primary ID are
+ * considered equal. In particular, secondary IDs vector may get updated for
+ * an entry on the program list (ie. when a better frequency for a given
+ * station is found).
+ *
+ * The primaryId of a given programType MUST be of a specific type:
+ * - AM, FM: RDS_PI if the station broadcasts RDS, AMFM_FREQUENCY otherwise;
+ * - AM_HD, FM_HD: HD_STATION_ID_EXT;
+ * - DAB: DAB_SIDECC;
+ * - DRMO: DRMO_SERVICE_ID;
+ * - SXM: SXM_SERVICE_ID;
+ * - VENDOR: VENDOR_PRIMARY.
+ * @hide
+ */
+@SystemApi
+public final class ProgramSelector implements Parcelable {
+    /** Analogue AM radio (with or without RDS). */
+    public static final int PROGRAM_TYPE_AM = 1;
+    /** analogue FM radio (with or without RDS). */
+    public static final int PROGRAM_TYPE_FM = 2;
+    /** AM HD Radio. */
+    public static final int PROGRAM_TYPE_AM_HD = 3;
+    /** FM HD Radio. */
+    public static final int PROGRAM_TYPE_FM_HD = 4;
+    /** Digital audio broadcasting. */
+    public static final int PROGRAM_TYPE_DAB = 5;
+    /** Digital Radio Mondiale. */
+    public static final int PROGRAM_TYPE_DRMO = 6;
+    /** SiriusXM Satellite Radio. */
+    public static final int PROGRAM_TYPE_SXM = 7;
+    /** Vendor-specific, not synced across devices. */
+    public static final int PROGRAM_TYPE_VENDOR1 = 8;
+    public static final int PROGRAM_TYPE_VENDOR2 = 9;
+    public static final int PROGRAM_TYPE_VENDOR3 = 10;
+    public static final int PROGRAM_TYPE_VENDOR4 = 11;
+    @IntDef(prefix = { "PROGRAM_TYPE_" }, value = {
+        PROGRAM_TYPE_AM,
+        PROGRAM_TYPE_FM,
+        PROGRAM_TYPE_AM_HD,
+        PROGRAM_TYPE_FM_HD,
+        PROGRAM_TYPE_DAB,
+        PROGRAM_TYPE_DRMO,
+        PROGRAM_TYPE_SXM,
+        PROGRAM_TYPE_VENDOR1,
+        PROGRAM_TYPE_VENDOR2,
+        PROGRAM_TYPE_VENDOR3,
+        PROGRAM_TYPE_VENDOR4,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ProgramType {}
+
+    /** kHz */
+    public static final int IDENTIFIER_TYPE_AMFM_FREQUENCY = 1;
+    /** 16bit */
+    public static final int IDENTIFIER_TYPE_RDS_PI = 2;
+    /**
+     * 64bit compound primary identifier for HD Radio.
+     *
+     * Consists of (from the LSB):
+     * - 32bit: Station ID number;
+     * - 4bit: HD_SUBCHANNEL;
+     * - 18bit: AMFM_FREQUENCY.
+     * The remaining bits should be set to zeros when writing on the chip side
+     * and ignored when read.
+     */
+    public static final int IDENTIFIER_TYPE_HD_STATION_ID_EXT = 3;
+    /**
+     * HD Radio subchannel - a value of range 0-7.
+     *
+     * The subchannel index is 0-based (where 0 is MPS and 1..7 are SPS),
+     * as opposed to HD Radio standard (where it's 1-based).
+     */
+    public static final int IDENTIFIER_TYPE_HD_SUBCHANNEL = 4;
+    /**
+     * 24bit compound primary identifier for DAB.
+     *
+     * Consists of (from the LSB):
+     * - 16bit: SId;
+     * - 8bit: ECC code.
+     * The remaining bits should be set to zeros when writing on the chip side
+     * and ignored when read.
+     */
+    public static final int IDENTIFIER_TYPE_DAB_SIDECC = 5;
+    /** 16bit */
+    public static final int IDENTIFIER_TYPE_DAB_ENSEMBLE = 6;
+    /** 12bit */
+    public static final int IDENTIFIER_TYPE_DAB_SCID = 7;
+    /** kHz */
+    public static final int IDENTIFIER_TYPE_DAB_FREQUENCY = 8;
+    /** 24bit */
+    public static final int IDENTIFIER_TYPE_DRMO_SERVICE_ID = 9;
+    /** kHz */
+    public static final int IDENTIFIER_TYPE_DRMO_FREQUENCY = 10;
+    /** 32bit */
+    public static final int IDENTIFIER_TYPE_SXM_SERVICE_ID = 11;
+    /** 0-999 range */
+    public static final int IDENTIFIER_TYPE_SXM_CHANNEL = 12;
+    /**
+     * Primary identifier for vendor-specific radio technology.
+     * The value format is determined by a vendor.
+     *
+     * It must not be used in any other programType than VENDORx.
+     */
+    public static final int IDENTIFIER_TYPE_VENDOR1_PRIMARY = 13;
+    public static final int IDENTIFIER_TYPE_VENDOR2_PRIMARY = 14;
+    public static final int IDENTIFIER_TYPE_VENDOR3_PRIMARY = 15;
+    public static final int IDENTIFIER_TYPE_VENDOR4_PRIMARY = 16;
+    @IntDef(prefix = { "IDENTIFIER_TYPE_" }, value = {
+        IDENTIFIER_TYPE_AMFM_FREQUENCY,
+        IDENTIFIER_TYPE_RDS_PI,
+        IDENTIFIER_TYPE_HD_STATION_ID_EXT,
+        IDENTIFIER_TYPE_HD_SUBCHANNEL,
+        IDENTIFIER_TYPE_DAB_SIDECC,
+        IDENTIFIER_TYPE_DAB_ENSEMBLE,
+        IDENTIFIER_TYPE_DAB_SCID,
+        IDENTIFIER_TYPE_DAB_FREQUENCY,
+        IDENTIFIER_TYPE_DRMO_SERVICE_ID,
+        IDENTIFIER_TYPE_DRMO_FREQUENCY,
+        IDENTIFIER_TYPE_SXM_SERVICE_ID,
+        IDENTIFIER_TYPE_SXM_CHANNEL,
+        IDENTIFIER_TYPE_VENDOR1_PRIMARY,
+        IDENTIFIER_TYPE_VENDOR2_PRIMARY,
+        IDENTIFIER_TYPE_VENDOR3_PRIMARY,
+        IDENTIFIER_TYPE_VENDOR4_PRIMARY,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface IdentifierType {}
+
+    private final @ProgramType int mProgramType;
+    private final @NonNull Identifier mPrimaryId;
+    private final @NonNull Identifier[] mSecondaryIds;
+    private final @NonNull long[] mVendorIds;
+
+    /**
+     * Constructor for ProgramSelector.
+     *
+     * It's not desired to modify selector objects, so all its fields are initialized at creation.
+     *
+     * Identifier lists must not contain any nulls, but can itself be null to be interpreted
+     * as empty list at object creation.
+     *
+     * @param programType type of a radio technology.
+     * @param primaryId primary program identifier.
+     * @param secondaryIds list of secondary program identifiers.
+     * @param vendorIds list of vendor-specific program identifiers.
+     */
+    public ProgramSelector(@ProgramType int programType, @NonNull Identifier primaryId,
+            @Nullable Identifier[] secondaryIds, @Nullable long[] vendorIds) {
+        if (secondaryIds == null) secondaryIds = new Identifier[0];
+        if (vendorIds == null) vendorIds = new long[0];
+        if (Stream.of(secondaryIds).anyMatch(id -> id == null)) {
+            throw new IllegalArgumentException("secondaryIds list must not contain nulls");
+        }
+        mProgramType = programType;
+        mPrimaryId = Objects.requireNonNull(primaryId);
+        mSecondaryIds = secondaryIds;
+        mVendorIds = vendorIds;
+    }
+
+    /**
+     * Type of a radio technology.
+     *
+     * @returns program type.
+     */
+    public @ProgramType int getProgramType() {
+        return mProgramType;
+    }
+
+    /**
+     * Primary program identifier uniquely identifies a station and is used to
+     * determine equality between two ProgramSelectors.
+     *
+     * @returns primary identifier.
+     */
+    public @NonNull Identifier getPrimaryId() {
+        return mPrimaryId;
+    }
+
+    /**
+     * Secondary program identifier is not required for tuning, but may make it
+     * faster or more reliable.
+     *
+     * @returns secondary identifier list, must not be modified.
+     */
+    public @NonNull Identifier[] getSecondaryIds() {
+        return mSecondaryIds;
+    }
+
+    /**
+     * Looks up an identifier of a given type (either primary or secondary).
+     *
+     * If there are multiple identifiers if a given type, then first in order (where primary id is
+     * before any secondary) is selected.
+     *
+     * @param type type of identifier.
+     * @return identifier value, if found.
+     * @throws IllegalArgumentException, if not found.
+     */
+    public long getFirstId(@IdentifierType int type) {
+        if (mPrimaryId.getType() == type) return mPrimaryId.getValue();
+        for (Identifier id : mSecondaryIds) {
+            if (id.getType() == type) return id.getValue();
+        }
+        throw new IllegalArgumentException("Identifier " + type + " not found");
+    }
+
+    /**
+     * Looks up all identifier of a given type (either primary or secondary).
+     *
+     * Some identifiers may be provided multiple times, for example
+     * IDENTIFIER_TYPE_AMFM_FREQUENCY for FM Alternate Frequencies.
+     *
+     * @param type type of identifier.
+     * @return a list of identifiers, generated on each call. May be modified.
+     */
+    public @NonNull Identifier[] getAllIds(@IdentifierType int type) {
+        List<Identifier> out = new ArrayList<>();
+
+        if (mPrimaryId.getType() == type) out.add(mPrimaryId);
+        for (Identifier id : mSecondaryIds) {
+            if (id.getType() == type) out.add(id);
+        }
+
+        return out.toArray(new Identifier[out.size()]);
+    }
+
+    /**
+     * Vendor identifiers are passed as-is to the HAL implementation,
+     * preserving elements order.
+     *
+     * @return a array of vendor identifiers, must not be modified.
+     */
+    public @NonNull long[] getVendorIds() {
+        return mVendorIds;
+    }
+
+    /**
+     * Builds new ProgramSelector for AM/FM frequency.
+     *
+     * @param band the band.
+     * @param frequencyKhz the frequency in kHz.
+     * @return new ProgramSelector object representing given frequency.
+     * @throws IllegalArgumentException if provided frequency is out of bounds.
+     */
+    public static @NonNull ProgramSelector createAmFmSelector(
+            @RadioManager.Band int band, int frequencyKhz) {
+        return createAmFmSelector(band, frequencyKhz, 0);
+    }
+
+    /**
+     * Checks, if a given AM/FM frequency is roughly valid and in correct unit.
+     *
+     * It does not check the range precisely. In particular, it may be way off for certain regions.
+     * The main purpose is to avoid passing inproper units, ie. MHz instead of kHz.
+     *
+     * @param isAm true, if AM, false if FM.
+     * @param frequencyKhz the frequency in kHz.
+     * @return true, if the frequency is rougly valid.
+     */
+    private static boolean isValidAmFmFrequency(boolean isAm, int frequencyKhz) {
+        if (isAm) {
+            return frequencyKhz > 150 && frequencyKhz < 30000;
+        } else {
+            return frequencyKhz > 60000 && frequencyKhz < 110000;
+        }
+    }
+
+    /**
+     * Builds new ProgramSelector for AM/FM frequency.
+     *
+     * This method variant supports HD Radio subchannels, but it's undesirable to
+     * select them manually. Instead, the value should be retrieved from program list.
+     *
+     * @param band the band.
+     * @param frequencyKhz the frequency in kHz.
+     * @param subChannel 1-based HD Radio subchannel.
+     * @return new ProgramSelector object representing given frequency.
+     * @throws IllegalArgumentException if provided frequency is out of bounds,
+     *         or tried setting a subchannel for analog AM/FM.
+     */
+    public static @NonNull ProgramSelector createAmFmSelector(
+            @RadioManager.Band int band, int frequencyKhz, int subChannel) {
+        boolean isAm = (band == RadioManager.BAND_AM || band == RadioManager.BAND_AM_HD);
+        boolean isDigital = (band == RadioManager.BAND_AM_HD || band == RadioManager.BAND_FM_HD);
+        if (!isAm && !isDigital && band != RadioManager.BAND_FM) {
+            throw new IllegalArgumentException("Unknown band: " + band);
+        }
+        if (subChannel < 0 || subChannel > 8) {
+            throw new IllegalArgumentException("Invalid subchannel: " + subChannel);
+        }
+        if (subChannel > 0 && !isDigital) {
+            throw new IllegalArgumentException("Subchannels are not supported for non-HD radio");
+        }
+        if (!isValidAmFmFrequency(isAm, frequencyKhz)) {
+            throw new IllegalArgumentException("Provided value is not a valid AM/FM frequency");
+        }
+
+        // We can't use AM_HD or FM_HD, because we don't know HD station ID.
+        @ProgramType int programType = isAm ? PROGRAM_TYPE_AM : PROGRAM_TYPE_FM;
+        Identifier primary = new Identifier(IDENTIFIER_TYPE_AMFM_FREQUENCY, frequencyKhz);
+
+        Identifier[] secondary = null;
+        if (subChannel > 0) {
+            /* Stating sub channel for non-HD AM/FM does not give any guarantees,
+             * but we can't do much more without HD station ID.
+             *
+             * The legacy APIs had 1-based subChannels, while ProgramSelector is 0-based.
+             */
+            secondary = new Identifier[]{
+                    new Identifier(IDENTIFIER_TYPE_HD_SUBCHANNEL, subChannel - 1)};
+        }
+
+        return new ProgramSelector(programType, primary, secondary, null);
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder("ProgramSelector(type=").append(mProgramType)
+                .append(", primary=").append(mPrimaryId);
+        if (mSecondaryIds.length > 0) sb.append(", secondary=").append(mSecondaryIds);
+        if (mVendorIds.length > 0) sb.append(", vendor=").append(mVendorIds);
+        sb.append(")");
+        return sb.toString();
+    }
+
+    @Override
+    public int hashCode() {
+        // secondaryIds and vendorIds are ignored for equality/hashing
+        return Objects.hash(mProgramType, mPrimaryId);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) return true;
+        if (!(obj instanceof ProgramSelector)) return false;
+        ProgramSelector other = (ProgramSelector) obj;
+        // secondaryIds and vendorIds are ignored for equality/hashing
+        return other.getProgramType() == mProgramType && mPrimaryId.equals(other.getPrimaryId());
+    }
+
+    private ProgramSelector(Parcel in) {
+        mProgramType = in.readInt();
+        mPrimaryId = in.readTypedObject(Identifier.CREATOR);
+        mSecondaryIds = in.createTypedArray(Identifier.CREATOR);
+        if (Stream.of(mSecondaryIds).anyMatch(id -> id == null)) {
+            throw new IllegalArgumentException("secondaryIds list must not contain nulls");
+        }
+        mVendorIds = in.createLongArray();
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeInt(mProgramType);
+        dest.writeTypedObject(mPrimaryId, 0);
+        dest.writeTypedArray(mSecondaryIds, 0);
+        dest.writeLongArray(mVendorIds);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    public static final Parcelable.Creator<ProgramSelector> CREATOR =
+            new Parcelable.Creator<ProgramSelector>() {
+        public ProgramSelector createFromParcel(Parcel in) {
+            return new ProgramSelector(in);
+        }
+
+        public ProgramSelector[] newArray(int size) {
+            return new ProgramSelector[size];
+        }
+    };
+
+    /**
+     * A single program identifier component, eg. frequency or channel ID.
+     *
+     * The long value field holds the value in format described in comments for
+     * IdentifierType constants.
+     */
+    public static final class Identifier implements Parcelable {
+        private final @IdentifierType int mType;
+        private final long mValue;
+
+        public Identifier(@IdentifierType int type, long value) {
+            mType = type;
+            mValue = value;
+        }
+
+        /**
+         * Type of an identifier.
+         *
+         * @return type of an identifier.
+         */
+        public @IdentifierType int getType() {
+            return mType;
+        }
+
+        /**
+         * Value of an identifier.
+         *
+         * Its meaning depends on identifier type, ie. for IDENTIFIER_TYPE_AMFM_FREQUENCY type,
+         * the value is a frequency in kHz.
+         *
+         * The range of a value depends on its type; it does not always require the whole long
+         * range. Casting to necessary type (ie. int) without range checking is correct in front-end
+         * code - any range violations are either errors in the framework or in the
+         * HAL implementation. For example, IDENTIFIER_TYPE_AMFM_FREQUENCY always fits in int,
+         * as Integer.MAX_VALUE would mean 2.1THz.
+         *
+         * @return value of an identifier.
+         */
+        public long getValue() {
+            return mValue;
+        }
+
+        @Override
+        public String toString() {
+            return "Identifier(" + mType + ", " + mValue + ")";
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(mType, mValue);
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (this == obj) return true;
+            if (!(obj instanceof Identifier)) return false;
+            Identifier other = (Identifier) obj;
+            return other.getType() == mType && other.getValue() == mValue;
+        }
+
+        private Identifier(Parcel in) {
+            mType = in.readInt();
+            mValue = in.readLong();
+        }
+
+        @Override
+        public void writeToParcel(Parcel dest, int flags) {
+            dest.writeInt(mType);
+            dest.writeLong(mValue);
+        }
+
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+
+        public static final Parcelable.Creator<Identifier> CREATOR =
+                new Parcelable.Creator<Identifier>() {
+            public Identifier createFromParcel(Parcel in) {
+                return new Identifier(in);
+            }
+
+            public Identifier[] newArray(int size) {
+                return new Identifier[size];
+            }
+        };
+    }
+}
diff --git a/core/java/android/hardware/radio/RadioManager.java b/core/java/android/hardware/radio/RadioManager.java
index d25e752..8ef5705 100644
--- a/core/java/android/hardware/radio/RadioManager.java
+++ b/core/java/android/hardware/radio/RadioManager.java
@@ -16,6 +16,7 @@
 
 package android.hardware.radio;
 
+import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.SystemApi;
 import android.annotation.SystemService;
@@ -29,6 +30,8 @@
 import android.text.TextUtils;
 import android.util.Log;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.util.Arrays;
 import java.util.List;
 
@@ -70,7 +73,7 @@
     /** Radio module class supporting Digital terrestrial radio */
     public static final int CLASS_DT = 2;
 
-    // keep in sync with radio_band_t in /system/core/incluse/system/radio.h
+    public static final int BAND_INVALID = -1;
     /** AM radio band (LW/MW/SW).
      * @see BandDescriptor */
     public static final int BAND_AM = 0;
@@ -83,6 +86,15 @@
     /** 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.
@@ -1258,8 +1270,7 @@
         private final static int FLAG_LIVE = 1 << 0;
         private final static int FLAG_MUTED = 1 << 1;
 
-        private final int mChannel;
-        private final int mSubChannel;
+        @NonNull private final ProgramSelector mSelector;
         private final boolean mTuned;
         private final boolean mStereo;
         private final boolean mDigital;
@@ -1268,11 +1279,10 @@
         private final RadioMetadata mMetadata;
         private final String mVendorExension;
 
-        ProgramInfo(int channel, int subChannel, boolean tuned, boolean stereo,
+        ProgramInfo(@NonNull ProgramSelector selector, boolean tuned, boolean stereo,
                 boolean digital, int signalStrength, RadioMetadata metadata, int flags,
                 String vendorExension) {
-            mChannel = channel;
-            mSubChannel = subChannel;
+            mSelector = selector;
             mTuned = tuned;
             mStereo = stereo;
             mDigital = digital;
@@ -1282,19 +1292,45 @@
             mVendorExension = vendorExension;
         }
 
+        /**
+         * Program selector, necessary for tuning to a program.
+         *
+         * @return the program selector.
+         */
+        public @NonNull ProgramSelector getSelector() {
+            return mSelector;
+        }
+
         /** 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() {
-            return mChannel;
+            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() {
-            return mSubChannel;
+            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.
          */
@@ -1362,8 +1398,7 @@
         }
 
         private ProgramInfo(Parcel in) {
-            mChannel = in.readInt();
-            mSubChannel = in.readInt();
+            mSelector = in.readParcelable(null);
             mTuned = in.readByte() == 1;
             mStereo = in.readByte() == 1;
             mDigital = in.readByte() == 1;
@@ -1390,8 +1425,7 @@
 
         @Override
         public void writeToParcel(Parcel dest, int flags) {
-            dest.writeInt(mChannel);
-            dest.writeInt(mSubChannel);
+            dest.writeParcelable(mSelector, 0);
             dest.writeByte((byte)(mTuned ? 1 : 0));
             dest.writeByte((byte)(mStereo ? 1 : 0));
             dest.writeByte((byte)(mDigital ? 1 : 0));
@@ -1413,7 +1447,7 @@
 
         @Override
         public String toString() {
-            return "ProgramInfo [mChannel=" + mChannel + ", mSubChannel=" + mSubChannel
+            return "ProgramInfo [mSelector=" + mSelector
                     + ", mTuned=" + mTuned + ", mStereo=" + mStereo + ", mDigital=" + mDigital
                     + ", mFlags=" + mFlags + ", mSignalStrength=" + mSignalStrength
                     + ((mMetadata == null) ? "" : (", mMetadata=" + mMetadata.toString()))
@@ -1424,8 +1458,7 @@
         public int hashCode() {
             final int prime = 31;
             int result = 1;
-            result = prime * result + mChannel;
-            result = prime * result + mSubChannel;
+            result = prime * result + mSelector.hashCode();
             result = prime * result + (mTuned ? 1 : 0);
             result = prime * result + (mStereo ? 1 : 0);
             result = prime * result + (mDigital ? 1 : 0);
@@ -1443,10 +1476,7 @@
             if (!(obj instanceof ProgramInfo))
                 return false;
             ProgramInfo other = (ProgramInfo) obj;
-            if (mChannel != other.getChannel())
-                return false;
-            if (mSubChannel != other.getSubChannel())
-                return false;
+            if (!mSelector.equals(other.getSelector())) return false;
             if (mTuned != other.isTuned())
                 return false;
             if (mStereo != other.isStereo())
@@ -1541,7 +1571,7 @@
             Log.e(TAG, "Failed to open tuner");
             return null;
         }
-        return new TunerAdapter(tuner);
+        return new TunerAdapter(tuner, config != null ? config.getType() : BAND_INVALID);
     }
 
     @NonNull private final Context mContext;
diff --git a/core/java/android/hardware/radio/RadioTuner.java b/core/java/android/hardware/radio/RadioTuner.java
index 61e62ea..f659273 100644
--- a/core/java/android/hardware/radio/RadioTuner.java
+++ b/core/java/android/hardware/radio/RadioTuner.java
@@ -170,10 +170,22 @@
      *  <li>{@link RadioManager#STATUS_DEAD_OBJECT} if the binder transaction to the native
      *  service fails, </li>
      * </ul>
+     * @deprecated Use {@link tune(ProgramSelector)} instead.
      */
+    @Deprecated
     public abstract int tune(int channel, int subChannel);
 
     /**
+     * Tune to a program.
+     *
+     * The operation is asynchronous and {@link Callback} onProgramInfoChanged() will be called
+     * when tune completes or onError() when cancelled or on timeout.
+     *
+     * @thows IllegalArgumentException if the provided selector is invalid
+     */
+    public abstract void tune(@NonNull ProgramSelector selector);
+
+    /**
      * Cancel a pending scan or tune operation.
      * If an operation is pending, {@link Callback} onError() will be called with
      * {@link #ERROR_CANCELLED}.
diff --git a/core/java/android/hardware/radio/TunerAdapter.java b/core/java/android/hardware/radio/TunerAdapter.java
index dbaca62..503bb55 100644
--- a/core/java/android/hardware/radio/TunerAdapter.java
+++ b/core/java/android/hardware/radio/TunerAdapter.java
@@ -32,11 +32,14 @@
     @NonNull private final ITuner mTuner;
     private boolean mIsClosed = false;
 
-    TunerAdapter(ITuner tuner) {
+    private @RadioManager.Band int mBand;
+
+    TunerAdapter(ITuner tuner, @RadioManager.Band int band) {
         if (tuner == null) {
             throw new NullPointerException();
         }
         mTuner = tuner;
+        mBand = band;
     }
 
     @Override
@@ -59,6 +62,7 @@
     public int setConfiguration(RadioManager.BandConfig config) {
         try {
             mTuner.setConfiguration(config);
+            mBand = config.getType();
             return RadioManager.STATUS_OK;
         } catch (IllegalArgumentException e) {
             Log.e(TAG, "Can't set configuration", e);
@@ -138,7 +142,7 @@
     @Override
     public int tune(int channel, int subChannel) {
         try {
-            mTuner.tune(channel, subChannel);
+            mTuner.tune(ProgramSelector.createAmFmSelector(mBand, channel, subChannel));
         } catch (IllegalStateException e) {
             Log.e(TAG, "Can't tune", e);
             return RadioManager.STATUS_INVALID_OPERATION;
@@ -153,6 +157,15 @@
     }
 
     @Override
+    public void tune(@NonNull ProgramSelector selector) {
+        try {
+            mTuner.tune(selector);
+        } catch (RemoteException e) {
+            throw new RuntimeException("service died", e);
+        }
+    }
+
+    @Override
     public int cancel() {
         try {
             mTuner.cancel();
diff --git a/services/core/java/com/android/server/radio/Tuner.java b/services/core/java/com/android/server/radio/Tuner.java
index 81128c2..209febc 100644
--- a/services/core/java/com/android/server/radio/Tuner.java
+++ b/services/core/java/com/android/server/radio/Tuner.java
@@ -19,6 +19,7 @@
 import android.annotation.NonNull;
 import android.hardware.radio.ITuner;
 import android.hardware.radio.ITunerCallback;
+import android.hardware.radio.ProgramSelector;
 import android.hardware.radio.RadioManager;
 import android.os.IBinder;
 import android.os.RemoteException;
@@ -44,12 +45,13 @@
     private int mRegion;  // TODO(b/62710330): find better solution to handle regions
     private final boolean mWithAudio;
 
-    Tuner(@NonNull ITunerCallback clientCallback, int halRev, int region, boolean withAudio) {
+    Tuner(@NonNull ITunerCallback clientCallback, int halRev,
+            int region, boolean withAudio, int band) {
         mClientCallback = clientCallback;
         mTunerCallback = new TunerCallback(this, clientCallback, halRev);
         mRegion = region;
         mWithAudio = withAudio;
-        mNativeContext = nativeInit(halRev, withAudio);
+        mNativeContext = nativeInit(halRev, withAudio, band);
         mDeathRecipient = this::close;
         try {
             mClientCallback.asBinder().linkToDeath(mDeathRecipient, 0);
@@ -64,7 +66,7 @@
         super.finalize();
     }
 
-    private native long nativeInit(int halRev, boolean withAudio);
+    private native long nativeInit(int halRev, boolean withAudio, int band);
     private native void nativeFinalize(long nativeContext);
     private native void nativeClose(long nativeContext);
 
@@ -74,7 +76,7 @@
 
     private native void nativeStep(long nativeContext, boolean directionDown, boolean skipSubChannel);
     private native void nativeScan(long nativeContext, boolean directionDown, boolean skipSubChannel);
-    private native void nativeTune(long nativeContext, int channel, int subChannel);
+    private native void nativeTune(long nativeContext, @NonNull ProgramSelector selector);
     private native void nativeCancel(long nativeContext);
 
     private native RadioManager.ProgramInfo nativeGetProgramInformation(long nativeContext);
@@ -173,10 +175,14 @@
     }
 
     @Override
-    public void tune(int channel, int subChannel) {
+    public void tune(ProgramSelector selector) {
+        if (selector == null) {
+            throw new IllegalArgumentException("The argument must not be a null pointer");
+        }
+        Slog.i(TAG, "Tuning to " + selector);
         synchronized (mLock) {
             checkNotClosedLocked();
-            nativeTune(mNativeContext, channel, subChannel);
+            nativeTune(mNativeContext, selector);
         }
     }
 
diff --git a/services/core/jni/Android.mk b/services/core/jni/Android.mk
index c8c629f..a6065025 100644
--- a/services/core/jni/Android.mk
+++ b/services/core/jni/Android.mk
@@ -108,4 +108,6 @@
     android.frameworks.schedulerservice@1.0 \
     android.frameworks.sensorservice@1.0 \
 
-LOCAL_STATIC_LIBRARIES += libscrypt_static
+LOCAL_STATIC_LIBRARIES += \
+    android.hardware.broadcastradio@1.1-utils-lib \
+    libscrypt_static \
diff --git a/services/core/jni/com_android_server_radio_RadioService.cpp b/services/core/jni/com_android_server_radio_RadioService.cpp
index fa64901..459c0d0 100644
--- a/services/core/jni/com_android_server_radio_RadioService.cpp
+++ b/services/core/jni/com_android_server_radio_RadioService.cpp
@@ -210,7 +210,7 @@
     BandConfig bandConfigHal = convert::BandConfigToHal(env, bandConfig, region);
 
     auto tuner = make_javaref(env, env->NewObject(gjni.Tuner.clazz, gjni.Tuner.cstor,
-            callback, halRev, region, withAudio));
+            callback, halRev, region, withAudio, bandConfigHal.type));
     if (tuner == nullptr) {
         ALOGE("Unable to create new tuner object.");
         return nullptr;
@@ -258,7 +258,7 @@
     auto tunerClass = FindClassOrDie(env, "com/android/server/radio/Tuner");
     gjni.Tuner.clazz = MakeGlobalRefOrDie(env, tunerClass);
     gjni.Tuner.cstor = GetMethodIDOrDie(env, tunerClass, "<init>",
-            "(Landroid/hardware/radio/ITunerCallback;IIZ)V");
+            "(Landroid/hardware/radio/ITunerCallback;IIZI)V");
 
     auto arrayListClass = FindClassOrDie(env, "java/util/ArrayList");
     gjni.ArrayList.clazz = MakeGlobalRefOrDie(env, arrayListClass);
diff --git a/services/core/jni/com_android_server_radio_Tuner.cpp b/services/core/jni/com_android_server_radio_Tuner.cpp
index 4b6f30b..13bfb57 100644
--- a/services/core/jni/com_android_server_radio_Tuner.cpp
+++ b/services/core/jni/com_android_server_radio_Tuner.cpp
@@ -22,12 +22,13 @@
 #include "com_android_server_radio_convert.h"
 #include "com_android_server_radio_TunerCallback.h"
 
+#include <JNIHelp.h>
+#include <Utils.h>
 #include <android/hardware/broadcastradio/1.1/IBroadcastRadioFactory.h>
 #include <binder/IPCThreadState.h>
 #include <core_jni_helpers.h>
 #include <media/AudioSystem.h>
 #include <utils/Log.h>
-#include <JNIHelp.h>
 
 namespace android {
 namespace server {
@@ -41,6 +42,7 @@
 namespace V1_0 = hardware::broadcastradio::V1_0;
 namespace V1_1 = hardware::broadcastradio::V1_1;
 
+using V1_0::Band;
 using V1_0::BandConfig;
 using V1_0::MetaData;
 using V1_0::Result;
@@ -79,6 +81,7 @@
     bool mIsClosed = false;
     HalRevision mHalRev;
     bool mWithAudio;
+    Band mBand;
     sp<V1_0::ITuner> mHalTuner;
     sp<V1_1::ITuner> mHalTuner11;
     sp<HalDeathRecipient> mHalDeathRecipient;
@@ -100,13 +103,14 @@
     return getNativeContext(env->GetLongField(jTuner.get(), gjni.Tuner.nativeContext));
 }
 
-static jlong nativeInit(JNIEnv *env, jobject obj, jint halRev, bool withAudio) {
+static jlong nativeInit(JNIEnv *env, jobject obj, jint halRev, bool withAudio, jint band) {
     ALOGV("nativeInit()");
     AutoMutex _l(gContextMutex);
 
     auto ctx = new TunerContext();
     ctx->mHalRev = static_cast<HalRevision>(halRev);
     ctx->mWithAudio = withAudio;
+    ctx->mBand = static_cast<Band>(band);
 
     static_assert(sizeof(jlong) >= sizeof(ctx), "jlong is smaller than a pointer");
     return reinterpret_cast<jlong>(ctx);
@@ -170,13 +174,17 @@
     notifyAudioService(ctx, true);
 }
 
-sp<V1_0::ITuner> getHalTuner(jlong nativeContext) {
-    AutoMutex _l(gContextMutex);
-    auto tuner = getNativeContext(nativeContext).mHalTuner;
+static sp<V1_0::ITuner> getHalTuner(const TunerContext& ctx) {
+    auto tuner = ctx.mHalTuner;
     LOG_ALWAYS_FATAL_IF(tuner == nullptr, "HAL tuner is not open");
     return tuner;
 }
 
+sp<V1_0::ITuner> getHalTuner(jlong nativeContext) {
+    AutoMutex _l(gContextMutex);
+    return getHalTuner(getNativeContext(nativeContext));
+}
+
 sp<V1_1::ITuner> getHalTuner11(jlong nativeContext) {
     AutoMutex _l(gContextMutex);
     return getNativeContext(nativeContext).mHalTuner11;
@@ -215,13 +223,17 @@
 
 static void nativeSetConfiguration(JNIEnv *env, jobject obj, jlong nativeContext, jobject config) {
     ALOGV("nativeSetConfiguration()");
-    auto halTuner = getHalTuner(nativeContext);
+    AutoMutex _l(gContextMutex);
+    auto& ctx = getNativeContext(nativeContext);
+    auto halTuner = getHalTuner(ctx);
     if (halTuner == nullptr) return;
 
     Region region_unused;
     BandConfig bandConfigHal = convert::BandConfigToHal(env, config, region_unused);
 
-    convert::ThrowIfFailed(env, halTuner->setConfiguration(bandConfigHal));
+    if (convert::ThrowIfFailed(env, halTuner->setConfiguration(bandConfigHal))) return;
+
+    ctx.mBand = bandConfigHal.type;
 }
 
 static jobject nativeGetConfiguration(JNIEnv *env, jobject obj, jlong nativeContext,
@@ -263,13 +275,26 @@
     convert::ThrowIfFailed(env, halTuner->scan(dir, skipSubChannel));
 }
 
-static void nativeTune(JNIEnv *env, jobject obj, jlong nativeContext,
-        jint channel, jint subChannel) {
-    ALOGV("nativeTune(%d, %d)", channel, subChannel);
-    auto halTuner = getHalTuner(nativeContext);
-    if (halTuner == nullptr) return;
+static void nativeTune(JNIEnv *env, jobject obj, jlong nativeContext, jobject jSelector) {
+    ALOGV("nativeTune()");
+    AutoMutex _l(gContextMutex);
+    auto& ctx = getNativeContext(nativeContext);
+    auto halTuner10 = getHalTuner(ctx);
+    auto halTuner11 = ctx.mHalTuner11;
+    if (halTuner10 == nullptr) return;
 
-    convert::ThrowIfFailed(env, halTuner->tune(channel, subChannel));
+    auto selector = convert::ProgramSelectorToHal(env, jSelector);
+    if (halTuner11 != nullptr) {
+        convert::ThrowIfFailed(env, halTuner11->tune_1_1(selector));
+    } else {
+        uint32_t channel, subChannel;
+        if (!V1_1::utils::getLegacyChannel(selector, &channel, &subChannel)) {
+            jniThrowException(env, "java/lang/IllegalArgumentException",
+                    "Can't tune to non-AM/FM channel with HAL<1.1");
+            return;
+        }
+        convert::ThrowIfFailed(env, halTuner10->tune(channel, subChannel));
+    }
 }
 
 static void nativeCancel(JNIEnv *env, jobject obj, jlong nativeContext) {
@@ -282,8 +307,10 @@
 
 static jobject nativeGetProgramInformation(JNIEnv *env, jobject obj, jlong nativeContext) {
     ALOGV("nativeGetProgramInformation()");
-    auto halTuner10 = getHalTuner(nativeContext);
-    auto halTuner11 = getHalTuner11(nativeContext);
+    AutoMutex _l(gContextMutex);
+    auto& ctx = getNativeContext(nativeContext);
+    auto halTuner10 = getHalTuner(ctx);
+    auto halTuner11 = ctx.mHalTuner11;
     if (halTuner10 == nullptr) return nullptr;
 
     JavaRef<jobject> jInfo;
@@ -301,7 +328,7 @@
                 const V1_0::ProgramInfo& info) {
             halResult = result;
             if (result != Result::OK) return;
-            jInfo = convert::ProgramInfoFromHal(env, info);
+            jInfo = convert::ProgramInfoFromHal(env, info, ctx.mBand);
         });
     }
 
@@ -402,7 +429,7 @@
 }
 
 static const JNINativeMethod gTunerMethods[] = {
-    { "nativeInit", "(IZ)J", (void*)nativeInit },
+    { "nativeInit", "(IZI)J", (void*)nativeInit },
     { "nativeFinalize", "(J)V", (void*)nativeFinalize },
     { "nativeClose", "(J)V", (void*)nativeClose },
     { "nativeSetConfiguration", "(JLandroid/hardware/radio/RadioManager$BandConfig;)V",
@@ -411,7 +438,7 @@
             (void*)nativeGetConfiguration },
     { "nativeStep", "(JZZ)V", (void*)nativeStep },
     { "nativeScan", "(JZZ)V", (void*)nativeScan },
-    { "nativeTune", "(JII)V", (void*)nativeTune },
+    { "nativeTune", "(JLandroid/hardware/radio/ProgramSelector;)V", (void*)nativeTune },
     { "nativeCancel", "(J)V", (void*)nativeCancel },
     { "nativeGetProgramInformation", "(J)Landroid/hardware/radio/RadioManager$ProgramInfo;",
             (void*)nativeGetProgramInformation },
diff --git a/services/core/jni/com_android_server_radio_convert.cpp b/services/core/jni/com_android_server_radio_convert.cpp
index 55fb10f..af000bd 100644
--- a/services/core/jni/com_android_server_radio_convert.cpp
+++ b/services/core/jni/com_android_server_radio_convert.cpp
@@ -19,9 +19,10 @@
 
 #include "com_android_server_radio_convert.h"
 
+#include <JNIHelp.h>
+#include <Utils.h>
 #include <core_jni_helpers.h>
 #include <utils/Log.h>
-#include <JNIHelp.h>
 
 namespace android {
 namespace server {
@@ -37,7 +38,9 @@
 using V1_0::MetadataType;
 using V1_0::Result;
 using V1_0::Rds;
+using V1_1::ProgramIdentifier;
 using V1_1::ProgramListResult;
+using V1_1::ProgramSelector;
 
 static JavaRef<jobject> BandDescriptorFromHal(JNIEnv *env, const V1_0::BandConfig &config, Region region);
 
@@ -90,6 +93,22 @@
     struct {
         jclass clazz;
         jmethodID cstor;
+        jfieldID programType;
+        jfieldID primaryId;
+        jfieldID secondaryIds;
+        jfieldID vendorIds;
+
+        struct {
+            jclass clazz;
+            jmethodID cstor;
+            jfieldID type;
+            jfieldID value;
+        } Identifier;
+    } ProgramSelector;
+
+    struct {
+        jclass clazz;
+        jmethodID cstor;
         jmethodID putIntFromNative;
         jmethodID putStringFromNative;
         jmethodID putBitmapFromNative;
@@ -383,25 +402,100 @@
     return jMetadata;
 }
 
+static JavaRef<jobject> ProgramIdentifierFromHal(JNIEnv *env, const ProgramIdentifier &id) {
+    ALOGV("ProgramIdentifierFromHal()");
+    return make_javaref(env, env->NewObject(gjni.ProgramSelector.Identifier.clazz,
+            gjni.ProgramSelector.Identifier.cstor, id.type, id.value));
+}
+
+static JavaRef<jobject> ProgramSelectorFromHal(JNIEnv *env, const ProgramSelector &selector) {
+    ALOGV("ProgramSelectorFromHal()");
+    auto jPrimary = ProgramIdentifierFromHal(env, selector.primaryId);
+
+    auto jSecondary = make_javaref(env, env->NewObjectArray(selector.secondaryIds.size(),
+            gjni.ProgramSelector.Identifier.clazz, nullptr));
+    for (size_t i = 0; i < selector.secondaryIds.size(); i++) {
+        auto jId = ProgramIdentifierFromHal(env, selector.secondaryIds[i]);
+        env->SetObjectArrayElement(jSecondary.get(), i, jId.get());
+    }
+
+    auto jVendor = make_javaref(env, env->NewLongArray(selector.vendorIds.size()));
+    auto jVendorElements = env->GetLongArrayElements(jVendor.get(), nullptr);
+    for (size_t i = 0; i < selector.vendorIds.size(); i++) {
+        jVendorElements[i] = selector.vendorIds[i];
+    }
+    env->ReleaseLongArrayElements(jVendor.get(), jVendorElements, 0);
+
+    return make_javaref(env, env->NewObject(gjni.ProgramSelector.clazz, gjni.ProgramSelector.cstor,
+            selector.programType, jPrimary.get(), jSecondary.get(), jVendor.get()));
+}
+
+static ProgramIdentifier ProgramIdentifierToHal(JNIEnv *env, jobject jId) {
+    ALOGV("ProgramIdentifierToHal()");
+
+    ProgramIdentifier id = {};
+    id.type = env->GetIntField(jId, gjni.ProgramSelector.Identifier.type);
+    id.value = env->GetLongField(jId, gjni.ProgramSelector.Identifier.value);
+    return id;
+}
+
+ProgramSelector ProgramSelectorToHal(JNIEnv *env, jobject jSelector) {
+    ALOGV("ProgramSelectorToHal()");
+
+    ProgramSelector selector = {};
+
+    selector.programType = env->GetIntField(jSelector, gjni.ProgramSelector.programType);
+
+    auto jPrimary = env->GetObjectField(jSelector, gjni.ProgramSelector.primaryId);
+    auto jSecondary = reinterpret_cast<jobjectArray>(
+            env->GetObjectField(jSelector, gjni.ProgramSelector.secondaryIds));
+    auto jVendor = reinterpret_cast<jlongArray>(
+            env->GetObjectField(jSelector, gjni.ProgramSelector.vendorIds));
+
+    if (jPrimary == nullptr || jSecondary == nullptr || jVendor == nullptr) {
+        ALOGE("ProgramSelector object is incomplete");
+        return {};
+    }
+
+    selector.primaryId = ProgramIdentifierToHal(env, jPrimary);
+    auto count = env->GetArrayLength(jSecondary);
+    selector.secondaryIds.resize(count);
+    for (jsize i = 0; i < count; i++) {
+        auto jId = env->GetObjectArrayElement(jSecondary, i);
+        selector.secondaryIds[i] = ProgramIdentifierToHal(env, jId);
+    }
+
+    count = env->GetArrayLength(jVendor);
+    selector.vendorIds.resize(count);
+    auto jVendorElements = env->GetLongArrayElements(jVendor, nullptr);
+    for (jint i = 0; i < count; i++) {
+        selector.vendorIds[i] = jVendorElements[i];
+    }
+    env->ReleaseLongArrayElements(jVendor, jVendorElements, 0);
+
+    return selector;
+}
+
 static JavaRef<jobject> ProgramInfoFromHal(JNIEnv *env, const V1_0::ProgramInfo &info10,
-        const V1_1::ProgramInfo *info11) {
+        const V1_1::ProgramInfo *info11, const ProgramSelector &selector) {
     ALOGV("ProgramInfoFromHal()");
 
     auto jMetadata = MetadataFromHal(env, info10.metadata);
     auto jVendorExtension = info11 ? make_javastr(env, info11->vendorExension) : nullptr;
+    auto jSelector = ProgramSelectorFromHal(env, selector);
 
     return make_javaref(env, env->NewObject(gjni.ProgramInfo.clazz, gjni.ProgramInfo.cstor,
-            info10.channel, info10.subChannel, info10.tuned, info10.stereo, info10.digital,
-            info10.signalStrength, jMetadata.get(), info11 ? info11->flags : 0,
-            jVendorExtension.get()));
+            jSelector.get(), info10.tuned, info10.stereo, info10.digital, info10.signalStrength,
+            jMetadata.get(), info11 ? info11->flags : 0, jVendorExtension.get()));
 }
 
-JavaRef<jobject> ProgramInfoFromHal(JNIEnv *env, const V1_0::ProgramInfo &info) {
-    return ProgramInfoFromHal(env, info, nullptr);
+JavaRef<jobject> ProgramInfoFromHal(JNIEnv *env, const V1_0::ProgramInfo &info, V1_0::Band band) {
+    auto selector = V1_1::utils::make_selector(band, info.channel, info.subChannel);
+    return ProgramInfoFromHal(env, info, nullptr, selector);
 }
 
 JavaRef<jobject> ProgramInfoFromHal(JNIEnv *env, const V1_1::ProgramInfo &info) {
-    return ProgramInfoFromHal(env, info.base, &info);
+    return ProgramInfoFromHal(env, info.base, &info, info.selector);
 }
 
 } // namespace convert
@@ -465,7 +559,31 @@
     auto programInfoClass = FindClassOrDie(env, "android/hardware/radio/RadioManager$ProgramInfo");
     gjni.ProgramInfo.clazz = MakeGlobalRefOrDie(env, programInfoClass);
     gjni.ProgramInfo.cstor = GetMethodIDOrDie(env, programInfoClass, "<init>",
-            "(IIZZZILandroid/hardware/radio/RadioMetadata;ILjava/lang/String;)V");
+            "(Landroid/hardware/radio/ProgramSelector;ZZZILandroid/hardware/radio/RadioMetadata;I"
+            "Ljava/lang/String;)V");
+
+    auto programSelectorClass = FindClassOrDie(env, "android/hardware/radio/ProgramSelector");
+    gjni.ProgramSelector.clazz = MakeGlobalRefOrDie(env, programSelectorClass);
+    gjni.ProgramSelector.cstor = GetMethodIDOrDie(env, programSelectorClass, "<init>",
+            "(ILandroid/hardware/radio/ProgramSelector$Identifier;"
+            "[Landroid/hardware/radio/ProgramSelector$Identifier;[J)V");
+    gjni.ProgramSelector.programType = GetFieldIDOrDie(env, programSelectorClass,
+            "mProgramType", "I");
+    gjni.ProgramSelector.primaryId = GetFieldIDOrDie(env, programSelectorClass,
+            "mPrimaryId", "Landroid/hardware/radio/ProgramSelector$Identifier;");
+    gjni.ProgramSelector.secondaryIds = GetFieldIDOrDie(env, programSelectorClass,
+            "mSecondaryIds", "[Landroid/hardware/radio/ProgramSelector$Identifier;");
+    gjni.ProgramSelector.vendorIds = GetFieldIDOrDie(env, programSelectorClass,
+            "mVendorIds", "[J");
+
+    auto progSelIdClass = FindClassOrDie(env, "android/hardware/radio/ProgramSelector$Identifier");
+    gjni.ProgramSelector.Identifier.clazz = MakeGlobalRefOrDie(env, progSelIdClass);
+    gjni.ProgramSelector.Identifier.cstor = GetMethodIDOrDie(env, progSelIdClass,
+            "<init>", "(IJ)V");
+    gjni.ProgramSelector.Identifier.type = GetFieldIDOrDie(env, progSelIdClass,
+            "mType", "I");
+    gjni.ProgramSelector.Identifier.value = GetFieldIDOrDie(env, progSelIdClass,
+            "mValue", "J");
 
     auto radioMetadataClass = FindClassOrDie(env, "android/hardware/radio/RadioMetadata");
     gjni.RadioMetadata.clazz = MakeGlobalRefOrDie(env, radioMetadataClass);
diff --git a/services/core/jni/com_android_server_radio_convert.h b/services/core/jni/com_android_server_radio_convert.h
index 3ba6fed..8b91ced 100644
--- a/services/core/jni/com_android_server_radio_convert.h
+++ b/services/core/jni/com_android_server_radio_convert.h
@@ -45,9 +45,10 @@
 V1_0::Direction DirectionToHal(bool directionDown);
 
 JavaRef<jobject> MetadataFromHal(JNIEnv *env, const hardware::hidl_vec<V1_0::MetaData> &metadata);
-JavaRef<jobject> ProgramInfoFromHal(JNIEnv *env, const V1_0::ProgramInfo &info);
+JavaRef<jobject> ProgramInfoFromHal(JNIEnv *env, const V1_0::ProgramInfo &info, V1_0::Band band);
 JavaRef<jobject> ProgramInfoFromHal(JNIEnv *env, const V1_1::ProgramInfo &info);
 
+V1_1::ProgramSelector ProgramSelectorToHal(JNIEnv *env, jobject jSelector);
 
 void ThrowParcelableRuntimeException(JNIEnv *env, const std::string& msg);
 
diff --git a/tests/radio/src/android/hardware/radio/tests/RadioTest.java b/tests/radio/src/android/hardware/radio/tests/RadioTest.java
index e354a34..0c6d0e2 100644
--- a/tests/radio/src/android/hardware/radio/tests/RadioTest.java
+++ b/tests/radio/src/android/hardware/radio/tests/RadioTest.java
@@ -18,6 +18,7 @@
 import android.Manifest;
 import android.content.Context;
 import android.content.pm.PackageManager;
+import android.hardware.radio.ProgramSelector;
 import android.hardware.radio.RadioManager;
 import android.hardware.radio.RadioTuner;
 import android.support.test.InstrumentationRegistry;
@@ -64,6 +65,7 @@
     private final int kConfigCallbackTimeoutMs = 10000;
     private final int kCancelTimeoutMs = 1000;
     private final int kTuneCallbackTimeoutMs = 30000;
+    private final int kFullScanTimeoutMs = 60000;
 
     private RadioManager mRadioManager;
     private RadioTuner mRadioTuner;
@@ -144,11 +146,9 @@
         assertNotNull(mRadioTuner);
         verify(mCallback, timeout(kConfigCallbackTimeoutMs)).onConfigurationChanged(any());
         resetCallback();
-    }
 
-    private void checkAntenna() {
-        boolean isConnected = mRadioTuner.isAntennaConnected();
-        assertTrue(isConnected);
+        boolean isAntennaConnected = mRadioTuner.isAntennaConnected();
+        assertTrue(isAntennaConnected);
     }
 
     @Test
@@ -256,7 +256,6 @@
     @Test
     public void testStep() {
         openTuner();
-        checkAntenna();
 
         int ret = mRadioTuner.step(RadioTuner.DIRECTION_DOWN, true);
         assertEquals(RadioManager.STATUS_OK, ret);
@@ -272,7 +271,6 @@
     @Test
     public void testTuneAndGetPI() {
         openTuner();
-        checkAntenna();
 
         int channel = mFmBandConfig.getLowerLimit() + mFmBandConfig.getSpacing();
 
@@ -304,7 +302,6 @@
     @Test
     public void testLateCancel() {
         openTuner();
-        checkAntenna();
 
         int ret = mRadioTuner.step(RadioTuner.DIRECTION_DOWN, false);
         assertEquals(RadioManager.STATUS_OK, ret);
@@ -317,7 +314,6 @@
     @Test
     public void testScanAndCancel() {
         openTuner();
-        checkAntenna();
 
         /* There is a possible race condition between scan and cancel commands - the scan may finish
          * before cancel command is issued. Thus we accept both outcomes in this test.
@@ -335,7 +331,6 @@
     @Test
     public void testStartBackgroundScan() {
         openTuner();
-        checkAntenna();
 
         boolean ret = mRadioTuner.startBackgroundScan();
         boolean isSupported = mModule.isBackgroundScanningSupported();
@@ -345,7 +340,6 @@
     @Test
     public void testGetProgramList() {
         openTuner();
-        checkAntenna();
 
         try {
             List<RadioManager.ProgramInfo> list = mRadioTuner.getProgramList(null);
@@ -357,6 +351,39 @@
     }
 
     @Test
+    public void testTuneFromProgramList() {
+        openTuner();
+
+        List<RadioManager.ProgramInfo> list;
+
+        try {
+            list = mRadioTuner.getProgramList(null);
+            assertNotNull(list);
+        } catch (IllegalStateException e) {
+            Log.i(TAG, "Background list is not ready, trying to fix it");
+
+            boolean success = mRadioTuner.startBackgroundScan();
+            assertTrue(success);
+            verify(mCallback, timeout(kFullScanTimeoutMs)).onBackgroundScanComplete();
+
+            list = mRadioTuner.getProgramList(null);
+            assertNotNull(list);
+        }
+
+        if (list.isEmpty()) {
+            Log.i(TAG, "Program list is empty, can't test tune");
+            return;
+        }
+
+        ProgramSelector sel = list.get(0).getSelector();
+        mRadioTuner.tune(sel);
+        ArgumentCaptor<RadioManager.ProgramInfo> infoc =
+                ArgumentCaptor.forClass(RadioManager.ProgramInfo.class);
+        verify(mCallback, timeout(kTuneCallbackTimeoutMs)).onProgramInfoChanged(infoc.capture());
+        assertEquals(sel, infoc.getValue().getSelector());
+    }
+
+    @Test
     public void testForcedAnalog() {
         openTuner();