Implement ProgramSelector at HIDL layer.

No front-end implementation yet.

Bug: b/32621193
Test: VTS
Change-Id: I48f034e709254836cad35bbeb4285c3c42a9e1cd
diff --git a/broadcastradio/1.1/ITuner.hal b/broadcastradio/1.1/ITuner.hal
index 7511629..912786a 100644
--- a/broadcastradio/1.1/ITuner.hal
+++ b/broadcastradio/1.1/ITuner.hal
@@ -21,6 +21,27 @@
 interface ITuner extends @1.0::ITuner {
 
     /**
+     * Tune to a specified program.
+     *
+     * For AM/FM, it must be called when a valid configuration has been applied.
+     * Automatically cancels pending scan, step or tune.
+     *
+     * If method returns OK, ITunerCallback.tuneComplete_1_1() MUST be called:
+     * - once successfully tuned;
+     * - after a time out;
+     * - after a full band scan, if no station found.
+     *
+     * The tuned field of ProgramInfo should indicate if tuned to a valid
+     * station or not.
+     *
+     * @param program Program to tune to.
+     * @return result OK if successfully started tunning.
+     *                INVALID_ARGUMENTS if invalid arguments are passed.
+     *                NOT_INITIALIZED if another error occurs.
+     */
+    tune_1_1(ProgramSelector program) generates (Result result);
+
+    /**
      * Retrieve current station information.
      * @return result OK if scan successfully started
      *                NOT_INITIALIZED if another error occurs
diff --git a/broadcastradio/1.1/default/Tuner.cpp b/broadcastradio/1.1/default/Tuner.cpp
index 9b39d36..bb07f36 100644
--- a/broadcastradio/1.1/default/Tuner.cpp
+++ b/broadcastradio/1.1/default/Tuner.cpp
@@ -20,6 +20,7 @@
 #include "BroadcastRadio.h"
 #include "Tuner.h"
 
+#include <Utils.h>
 #include <log/log.h>
 
 namespace android {
@@ -70,6 +71,8 @@
 
         mAmfmConfig = move(config);
         mAmfmConfig.antennaConnected = true;
+        mCurrentProgram = utils::make_selector(mAmfmConfig.type, mAmfmConfig.lowerLimit);
+
         mIsAmfmConfigSet = true;
         mCallback->configChange(Result::OK, mAmfmConfig);
     };
@@ -90,28 +93,31 @@
     return Void();
 }
 
-// makes ProgramInfo that points to no channel
-static ProgramInfo makeDummyProgramInfo(uint32_t channel) {
+// makes ProgramInfo that points to no program
+static ProgramInfo makeDummyProgramInfo(const ProgramSelector& selector) {
     ProgramInfo info11 = {};
     auto& info10 = info11.base;
 
-    info10.channel = channel;
+    utils::getLegacyChannel(selector, info10.channel, info10.subChannel);
+    info11.selector = selector;
     info11.flags |= ProgramInfoFlags::MUTED;
 
     return info11;
 }
 
-void Tuner::tuneInternalLocked() {
+void Tuner::tuneInternalLocked(const ProgramSelector& sel) {
     VirtualRadio* virtualRadio = nullptr;
     if (mAmfmConfig.type == Band::FM_HD || mAmfmConfig.type == Band::FM) {
         virtualRadio = &mVirtualFm;
     }
 
     VirtualProgram virtualProgram;
-    if (virtualRadio != nullptr && virtualRadio->getProgram(mCurrentProgram, virtualProgram)) {
+    if (virtualRadio != nullptr && virtualRadio->getProgram(sel, virtualProgram)) {
+        mCurrentProgram = virtualProgram.selector;
         mCurrentProgramInfo = static_cast<ProgramInfo>(virtualProgram);
     } else {
-        mCurrentProgramInfo = makeDummyProgramInfo(mCurrentProgram);
+        mCurrentProgram = sel;
+        mCurrentProgramInfo = makeDummyProgramInfo(sel);
     }
     mIsTuneCompleted = true;
 
@@ -152,7 +158,7 @@
     auto found = lower_bound(list.begin(), list.end(), VirtualProgram({current}));
     if (direction == Direction::UP) {
         if (found < list.end() - 1) {
-            if (found->channel == current) found++;
+            if (utils::tunesTo(current, found->selector)) found++;
         } else {
             found = list.begin();
         }
@@ -163,25 +169,31 @@
             found = list.end() - 1;
         }
     }
-    auto tuneTo = found->channel;
+    auto tuneTo = found->selector;
 
     mIsTuneCompleted = false;
     auto task = [this, tuneTo, direction]() {
         ALOGI("Performing scan %s", toString(direction).c_str());
 
         lock_guard<mutex> lk(mMut);
-        mCurrentProgram = tuneTo;
-        tuneInternalLocked();
+        tuneInternalLocked(tuneTo);
     };
     mThread.schedule(task, gDefaultDelay.scan);
 
     return Result::OK;
 }
 
-Return<Result> Tuner::step(Direction direction, bool skipSubChannel __unused) {
+Return<Result> Tuner::step(Direction direction, bool skipSubChannel) {
     ALOGV("%s", __func__);
+    ALOGW_IF(!skipSubChannel, "can't step to next frequency without ignoring subChannel");
 
     lock_guard<mutex> lk(mMut);
+
+    if (!utils::isAmFm(utils::getType(mCurrentProgram))) {
+        ALOGE("Can't step in anything else than AM/FM");
+        return Result::NOT_INITIALIZED;
+    }
+
     ALOGW_IF(!mIsAmfmConfigSet, "AM/FM config not set");
     if (!mIsAmfmConfigSet) return Result::INVALID_STATE;
     mIsTuneCompleted = false;
@@ -191,57 +203,73 @@
 
         lock_guard<mutex> lk(mMut);
 
+        auto current = utils::getId(mCurrentProgram, IdentifierType::AMFM_FREQUENCY, 0);
+
         if (direction == Direction::UP) {
-            mCurrentProgram += mAmfmConfig.spacings[0];
+            current += mAmfmConfig.spacings[0];
         } else {
-            mCurrentProgram -= mAmfmConfig.spacings[0];
+            current -= mAmfmConfig.spacings[0];
         }
 
-        if (mCurrentProgram > mAmfmConfig.upperLimit) mCurrentProgram = mAmfmConfig.lowerLimit;
-        if (mCurrentProgram < mAmfmConfig.lowerLimit) mCurrentProgram = mAmfmConfig.upperLimit;
+        if (current > mAmfmConfig.upperLimit) current = mAmfmConfig.lowerLimit;
+        if (current < mAmfmConfig.lowerLimit) current = mAmfmConfig.upperLimit;
 
-        tuneInternalLocked();
+        tuneInternalLocked(utils::make_selector(mAmfmConfig.type, current));
     };
     mThread.schedule(task, gDefaultDelay.step);
 
     return Result::OK;
 }
 
-Return<Result> Tuner::tune(uint32_t channel, uint32_t subChannel)  {
+Return<Result> Tuner::tune(uint32_t channel, uint32_t subChannel) {
     ALOGV("%s(%d, %d)", __func__, channel, subChannel);
+    Band band;
+    {
+        lock_guard<mutex> lk(mMut);
+        band = mAmfmConfig.type;
+    }
+    return tune_1_1(utils::make_selector(band, channel, subChannel));
+}
+
+Return<Result> Tuner::tune_1_1(const ProgramSelector& sel) {
+    ALOGV("%s(%s)", __func__, toString(sel).c_str());
 
     lock_guard<mutex> lk(mMut);
-    ALOGW_IF(!mIsAmfmConfigSet, "AM/FM config not set");
-    if (!mIsAmfmConfigSet) return Result::INVALID_STATE;
-    if (channel < mAmfmConfig.lowerLimit || channel > mAmfmConfig.upperLimit) {
-        return Result::INVALID_ARGUMENTS;
-    }
-    mIsTuneCompleted = false;
 
-    auto task = [this, channel]() {
+    if (utils::isAmFm(utils::getType(mCurrentProgram))) {
+        ALOGW_IF(!mIsAmfmConfigSet, "AM/FM config not set");
+        if (!mIsAmfmConfigSet) return Result::INVALID_STATE;
+
+        auto freq = utils::getId(sel, IdentifierType::AMFM_FREQUENCY);
+        if (freq < mAmfmConfig.lowerLimit || freq > mAmfmConfig.upperLimit) {
+            return Result::INVALID_ARGUMENTS;
+        }
+    }
+
+    mIsTuneCompleted = false;
+    auto task = [this, sel]() {
         lock_guard<mutex> lk(mMut);
-        mCurrentProgram = channel;
-        tuneInternalLocked();
+        tuneInternalLocked(sel);
     };
     mThread.schedule(task, gDefaultDelay.tune);
 
     return Result::OK;
 }
 
-Return<Result> Tuner::cancel()  {
+Return<Result> Tuner::cancel() {
     ALOGV("%s", __func__);
     mThread.cancelAll();
     return Result::OK;
 }
 
-Return<void> Tuner::getProgramInformation(getProgramInformation_cb _hidl_cb)  {
+Return<void> Tuner::getProgramInformation(getProgramInformation_cb _hidl_cb) {
     ALOGV("%s", __func__);
     return getProgramInformation_1_1([&](Result result, const ProgramInfo& info) {
         _hidl_cb(result, info.base);
     });
 }
 
-Return<void> Tuner::getProgramInformation_1_1(getProgramInformation_1_1_cb _hidl_cb)  {
+Return<void> Tuner::getProgramInformation_1_1(getProgramInformation_1_1_cb _hidl_cb) {
     ALOGV("%s", __func__);
 
     lock_guard<mutex> lk(mMut);
diff --git a/broadcastradio/1.1/default/Tuner.h b/broadcastradio/1.1/default/Tuner.h
index 7719d4d..17ee99f 100644
--- a/broadcastradio/1.1/default/Tuner.h
+++ b/broadcastradio/1.1/default/Tuner.h
@@ -39,6 +39,7 @@
     Return<Result> scan(V1_0::Direction direction, bool skipSubChannel) override;
     Return<Result> step(V1_0::Direction direction, bool skipSubChannel) override;
     Return<Result> tune(uint32_t channel, uint32_t subChannel) override;
+    Return<Result> tune_1_1(const ProgramSelector& program) override;
     Return<Result> cancel() override;
     Return<void> getProgramInformation(getProgramInformation_cb _hidl_cb) override;
     Return<void> getProgramInformation_1_1(getProgramInformation_1_1_cb _hidl_cb) override;
@@ -60,10 +61,10 @@
     bool mIsAmfmConfigSet = false;
     V1_0::BandConfig mAmfmConfig;
     bool mIsTuneCompleted = false;
-    uint32_t mCurrentProgram;  // TODO(b/32621193): Station Selector
+    ProgramSelector mCurrentProgram = {};
     ProgramInfo mCurrentProgramInfo = {};
 
-    void tuneInternalLocked();
+    void tuneInternalLocked(const ProgramSelector& sel);
 };
 
 }  // namespace implementation
diff --git a/broadcastradio/1.1/default/VirtualProgram.cpp b/broadcastradio/1.1/default/VirtualProgram.cpp
index df12a3e..ef8bd1c 100644
--- a/broadcastradio/1.1/default/VirtualProgram.cpp
+++ b/broadcastradio/1.1/default/VirtualProgram.cpp
@@ -15,6 +15,8 @@
  */
 #include "VirtualProgram.h"
 
+#include <Utils.h>
+
 namespace android {
 namespace hardware {
 namespace broadcastradio {
@@ -29,10 +31,12 @@
     ProgramInfo info11 = {};
     auto& info10 = info11.base;
 
-    info10.channel = channel;
+    utils::getLegacyChannel(selector, info10.channel, info10.subChannel);
+    info11.selector = selector;
     info10.tuned = true;
     info10.stereo = true;
-    info10.signalStrength = 100;
+    info10.digital = utils::isDigital(selector);
+    info10.signalStrength = info10.digital ? 100 : 80;
 
     info10.metadata = hidl_vec<MetaData>({
         {MetadataType::TEXT, MetadataKey::RDS_PS, {}, {}, programName, {}},
@@ -43,8 +47,25 @@
     return info11;
 }
 
+// Defining order on virtual programs, how they appear on band.
+// It's mostly for default implementation purposes, may not be complete or correct.
 bool operator<(const VirtualProgram& lhs, const VirtualProgram& rhs) {
-    return lhs.channel < rhs.channel;
+    auto& l = lhs.selector;
+    auto& r = rhs.selector;
+
+    // Two programs with the same primaryId is considered the same.
+    if (l.programType != r.programType) return l.programType < r.programType;
+    if (l.primaryId.type != r.primaryId.type) return l.primaryId.type < r.primaryId.type;
+    if (l.primaryId.value != r.primaryId.value) return l.primaryId.value < r.primaryId.value;
+
+    // A little exception for HD Radio subchannel - we check secondary ID too.
+    if (utils::hasId(l, IdentifierType::HD_SUBCHANNEL) &&
+        utils::hasId(r, IdentifierType::HD_SUBCHANNEL)) {
+        return utils::getId(l, IdentifierType::HD_SUBCHANNEL) <
+               utils::getId(r, IdentifierType::HD_SUBCHANNEL);
+    }
+
+    return false;
 }
 
 }  // namespace implementation
diff --git a/broadcastradio/1.1/default/VirtualProgram.h b/broadcastradio/1.1/default/VirtualProgram.h
index 303513f..a4fd72c 100644
--- a/broadcastradio/1.1/default/VirtualProgram.h
+++ b/broadcastradio/1.1/default/VirtualProgram.h
@@ -26,7 +26,7 @@
 namespace implementation {
 
 struct VirtualProgram {
-    uint32_t channel;  // TODO(b/32621193): Station Selector
+    ProgramSelector selector;
 
     std::string programName = "";
     std::string songArtist = "";
diff --git a/broadcastradio/1.1/default/VirtualRadio.cpp b/broadcastradio/1.1/default/VirtualRadio.cpp
index 0eab7ae..acf37a4 100644
--- a/broadcastradio/1.1/default/VirtualRadio.cpp
+++ b/broadcastradio/1.1/default/VirtualRadio.cpp
@@ -15,25 +15,31 @@
  */
 #include "VirtualRadio.h"
 
+#include <Utils.h>
+
 namespace android {
 namespace hardware {
 namespace broadcastradio {
 namespace V1_1 {
 namespace implementation {
 
+using V1_0::Band;
+
 using std::lock_guard;
 using std::move;
 using std::mutex;
 using std::vector;
 
+using utils::make_selector;
+
 vector<VirtualProgram> gInitialFmPrograms{
-    {94900, "Wild 94.9", "Drake ft. Rihanna", "Too Good"},
-    {96500, "KOIT", "Celine Dion", "All By Myself"},
-    {97300, "Alice@97.3", "Drops of Jupiter", "Train"},
-    {99700, "99.7 Now!", "The Chainsmokers", "Closer"},
-    {101300, "101-3 KISS-FM", "Justin Timberlake", "Rock Your Body"},
-    {103700, "iHeart80s @ 103.7", "Michael Jackson", "Billie Jean"},
-    {106100, "106 KMEL", "Drake", "Marvins Room"},
+    {make_selector(Band::FM, 94900), "Wild 94.9", "Drake ft. Rihanna", "Too Good"},
+    {make_selector(Band::FM, 96500), "KOIT", "Celine Dion", "All By Myself"},
+    {make_selector(Band::FM, 97300), "Alice@97.3", "Drops of Jupiter", "Train"},
+    {make_selector(Band::FM, 99700), "99.7 Now!", "The Chainsmokers", "Closer"},
+    {make_selector(Band::FM, 101300), "101-3 KISS-FM", "Justin Timberlake", "Rock Your Body"},
+    {make_selector(Band::FM, 103700), "iHeart80s @ 103.7", "Michael Jackson", "Billie Jean"},
+    {make_selector(Band::FM, 106100), "106 KMEL", "Drake", "Marvins Room"},
 };
 
 VirtualRadio::VirtualRadio(VirtualRadio&& o) : mPrograms(move(o.mPrograms)) {}
@@ -45,10 +51,10 @@
     return mPrograms;
 }
 
-bool VirtualRadio::getProgram(uint32_t channel, VirtualProgram& programOut) {
+bool VirtualRadio::getProgram(const ProgramSelector& selector, VirtualProgram& programOut) {
     lock_guard<mutex> lk(mMut);
     for (auto&& program : mPrograms) {
-        if (program.channel == channel) {
+        if (utils::tunesTo(selector, program.selector)) {
             programOut = program;
             return true;
         }
diff --git a/broadcastradio/1.1/default/VirtualRadio.h b/broadcastradio/1.1/default/VirtualRadio.h
index e1918a0..23cb06c 100644
--- a/broadcastradio/1.1/default/VirtualRadio.h
+++ b/broadcastradio/1.1/default/VirtualRadio.h
@@ -33,7 +33,7 @@
     VirtualRadio(std::vector<VirtualProgram> initialList);
 
     std::vector<VirtualProgram> getProgramList();
-    bool getProgram(uint32_t channel, VirtualProgram& program);
+    bool getProgram(const ProgramSelector& selector, VirtualProgram& program);
 
    private:
     std::mutex mMut;
diff --git a/broadcastradio/1.1/types.hal b/broadcastradio/1.1/types.hal
index 5577ea0..dee38b8 100644
--- a/broadcastradio/1.1/types.hal
+++ b/broadcastradio/1.1/types.hal
@@ -68,11 +68,156 @@
 };
 
 /**
+ * Type of a radio technology.
+ *
+ * All other values are reserved for future use.
+ */
+enum ProgramType : uint32_t {
+    AM = 1,  // analogue AM radio (with or without RDS)
+    FM,      // analogue FM radio (with or without RDS)
+    AM_HD,   // AM HD Radio
+    FM_HD,   // FM HD Radio
+    DAB,     // Digital audio broadcasting
+    DRMO,    // Digital Radio Mondiale
+    SXM,     // SiriusXM Satellite Radio
+    VENDOR,  // vendor-specific, not synced across devices
+};
+
+/**
+ * Type of program identifier component.
+ *
+ * It MUST match the radio technology for primary ID but does not have to match
+ * it for secondary IDs. For example, a satellite program may set AM/FM fallback
+ * frequency, if a station broadcasts both via satellite and AM/FM.
+ *
+ * The value format for each (but VENDOR_PRIMARY) identifier is strictly defined
+ * to maintain interoperability between devices made by different vendors.
+ *
+ * All other values are reserved for future use.
+ */
+enum IdentifierType : uint32_t {
+    AMFM_FREQUENCY = 1,  // kHz
+    RDS_PI,              // 16bit
+
+    /**
+     * 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.
+     */
+    HD_STATION_ID_EXT,
+
+    /**
+     * 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).
+     */
+    HD_SUBCHANNEL,
+
+    /**
+     * 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.
+     */
+    DAB_SIDECC,
+
+    DAB_ENSEMBLE,     // 16bit
+    DAB_SCID,         // 12bit
+    DAB_FREQUENCY,    // kHz
+    DRMO_SERVICE_ID,  // 24bit
+    DRMO_FREQUENCY,   // kHz
+    SXM_SERVICE_ID,   // 32bit
+    SXM_CHANNEL,      // 0-999 range
+
+    /**
+     * 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 VENDOR.
+     */
+    VENDOR_PRIMARY,
+};
+
+/**
+ * A single program identifier component, eg. frequency or channel ID.
+ *
+ * The uint32_t type field maps to IdentifierType enum. It's not straight,
+ * because the enum may be extended in future versions of the HAL. Values out of
+ * the enum range must not be used when writing and ignored when reading.
+ *
+ * The uint64_t value field holds the value in format described in comments for
+ * IdentifierType enum.
+ */
+struct ProgramIdentifier {
+    uint32_t type;  // IdentifierType
+    uint64_t value;
+};
+
+/**
+ * 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 uint32_t programType field maps to ProgramType enum. It's not straight,
+ * because the enum may be extended in future versions of the HAL. Values out of
+ * the enum range must not be used when writing and ignored when reading.
+ *
+ * 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.
+ */
+struct ProgramSelector {
+    uint32_t programType;  // ProgramType
+    ProgramIdentifier primaryId;  // uniquely identifies a station
+    vec<ProgramIdentifier> secondaryIds;
+
+    /**
+     * Opaque vendor-specific identifiers, to be passed to front-end
+     * without changes.
+     *
+     * The order is meaningful, ie. the first element may be defined as
+     * frequency, second as the subchannel etc.
+     *
+     * The vector is not serialized (either locally or to the cloud),
+     * unless it's a VENDOR program type.
+     */
+    vec<uint64_t> vendorIds;
+};
+
+/**
  * Radio program information. Returned by the HAL with event RADIO_EVENT_TUNED.
  * Contains information on currently tuned channel.
  */
 struct ProgramInfo {
     @1.0::ProgramInfo base;
+
+    ProgramSelector selector;
+
     bitfield<ProgramInfoFlags> flags;
 
     /**
diff --git a/broadcastradio/1.1/utils/Android.bp b/broadcastradio/1.1/utils/Android.bp
index fab6517..df5e8e0 100644
--- a/broadcastradio/1.1/utils/Android.bp
+++ b/broadcastradio/1.1/utils/Android.bp
@@ -24,7 +24,11 @@
         "-Werror",
     ],
     srcs: [
+        "Utils.cpp",
         "WorkerThread.cpp",
     ],
     export_include_dirs: ["."],
+    shared_libs: [
+        "android.hardware.broadcastradio@1.1",
+    ],
 }
diff --git a/broadcastradio/1.1/utils/Utils.cpp b/broadcastradio/1.1/utils/Utils.cpp
new file mode 100644
index 0000000..9dc0a53
--- /dev/null
+++ b/broadcastradio/1.1/utils/Utils.cpp
@@ -0,0 +1,207 @@
+/*
+ * 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.
+ */
+#define LOG_TAG "BroadcastRadioDefault.utils"
+//#define LOG_NDEBUG 0
+
+#include "Utils.h"
+
+#include <log/log.h>
+
+namespace android {
+namespace hardware {
+namespace broadcastradio {
+namespace V1_1 {
+namespace implementation {
+namespace utils {
+
+using V1_0::Band;
+
+static bool isCompatibleProgramType(const uint32_t ia, const uint32_t ib) {
+    auto a = static_cast<ProgramType>(ia);
+    auto b = static_cast<ProgramType>(ib);
+
+    if (a == b) return true;
+    if (a == ProgramType::AM && b == ProgramType::AM_HD) return true;
+    if (a == ProgramType::AM_HD && b == ProgramType::AM) return true;
+    if (a == ProgramType::FM && b == ProgramType::FM_HD) return true;
+    if (a == ProgramType::FM_HD && b == ProgramType::FM) return true;
+    return false;
+}
+
+static bool bothHaveId(const ProgramSelector& a, const ProgramSelector& b,
+                       const IdentifierType type) {
+    return hasId(a, type) && hasId(b, type);
+}
+
+static bool anyHaveId(const ProgramSelector& a, const ProgramSelector& b,
+                      const IdentifierType type) {
+    return hasId(a, type) || hasId(b, type);
+}
+
+static bool haveEqualIds(const ProgramSelector& a, const ProgramSelector& b,
+                         const IdentifierType type) {
+    if (!bothHaveId(a, b, type)) return false;
+    // TODO(b/36864090): we should check all Ids of a given type (ie. other AF), not just one
+    auto aId = getId(a, type);
+    auto bId = getId(b, type);
+    return aId == bId;
+}
+
+bool tunesTo(const ProgramSelector& a, const ProgramSelector& b) {
+    if (!isCompatibleProgramType(a.programType, b.programType)) return false;
+
+    auto type = getType(a);
+
+    switch (type) {
+        case ProgramType::AM:
+        case ProgramType::AM_HD:
+        case ProgramType::FM:
+        case ProgramType::FM_HD:
+            if (haveEqualIds(a, b, IdentifierType::HD_STATION_ID_EXT)) return true;
+
+            // if HD Radio subchannel is specified, it must match
+            if (anyHaveId(a, b, IdentifierType::HD_SUBCHANNEL)) {
+                // missing subchannel (analog) is an equivalent of first subchannel (MPS)
+                auto aCh = getId(a, IdentifierType::HD_SUBCHANNEL, 0);
+                auto bCh = getId(b, IdentifierType::HD_SUBCHANNEL, 0);
+                if (aCh != bCh) return false;
+            }
+
+            if (haveEqualIds(a, b, IdentifierType::RDS_PI)) return true;
+
+            return haveEqualIds(a, b, IdentifierType::AMFM_FREQUENCY);
+        case ProgramType::DAB:
+            return haveEqualIds(a, b, IdentifierType::DAB_SIDECC);
+        case ProgramType::DRMO:
+            return haveEqualIds(a, b, IdentifierType::DRMO_SERVICE_ID);
+        case ProgramType::SXM:
+            if (anyHaveId(a, b, IdentifierType::SXM_SERVICE_ID)) {
+                return haveEqualIds(a, b, IdentifierType::SXM_SERVICE_ID);
+            }
+            return haveEqualIds(a, b, IdentifierType::SXM_CHANNEL);
+        case ProgramType::VENDOR:
+        default:
+            ALOGW("Unsupported program type: %s", toString(type).c_str());
+            return false;
+    }
+}
+
+ProgramType getType(const ProgramSelector& sel) {
+    return static_cast<ProgramType>(sel.programType);
+}
+
+bool isAmFm(const ProgramType type) {
+    switch (type) {
+        case ProgramType::AM:
+        case ProgramType::FM:
+        case ProgramType::AM_HD:
+        case ProgramType::FM_HD:
+            return true;
+        default:
+            return false;
+    }
+}
+
+bool hasId(const ProgramSelector& sel, const IdentifierType type) {
+    auto itype = static_cast<uint32_t>(type);
+    if (sel.primaryId.type == itype) return true;
+    // not optimal, but we don't care in default impl
+    for (auto&& id : sel.secondaryIds) {
+        if (id.type == itype) return true;
+    }
+    return false;
+}
+
+uint64_t getId(const ProgramSelector& sel, const IdentifierType type) {
+    auto itype = static_cast<uint32_t>(type);
+    if (sel.primaryId.type == itype) return sel.primaryId.value;
+    // not optimal, but we don't care in default impl
+    for (auto&& id : sel.secondaryIds) {
+        if (id.type == itype) return id.value;
+    }
+    ALOGW("Identifier %s not found", toString(type).c_str());
+    return 0;
+}
+
+uint64_t getId(const ProgramSelector& sel, const IdentifierType type, uint64_t defval) {
+    if (!hasId(sel, type)) return defval;
+    return getId(sel, type);
+}
+
+ProgramSelector make_selector(Band band, uint32_t channel, uint32_t subChannel) {
+    ProgramSelector sel = {};
+
+    ALOGW_IF((subChannel > 0) && (band == Band::AM || band == Band::FM),
+             "got subChannel for non-HD AM/FM");
+
+    // we can't use ProgramType::AM_HD or FM_HD, because we don't know HD station ID
+    ProgramType type;
+    switch (band) {
+        case Band::AM:
+        case Band::AM_HD:
+            type = ProgramType::AM;
+            break;
+        case Band::FM:
+        case Band::FM_HD:
+            type = ProgramType::FM;
+            break;
+        default:
+            LOG_ALWAYS_FATAL("Unsupported band: %s", toString(band).c_str());
+    }
+
+    sel.programType = static_cast<uint32_t>(type);
+    sel.primaryId.type = static_cast<uint32_t>(IdentifierType::AMFM_FREQUENCY);
+    sel.primaryId.value = channel;
+    if (subChannel > 0) {
+        // stating sub channel for AM/FM channel does not give any guarantees,
+        // but we can't do much more without HD station ID
+        sel.secondaryIds = hidl_vec<ProgramIdentifier>{
+            {static_cast<uint32_t>(IdentifierType::HD_SUBCHANNEL), subChannel},
+        };
+    }
+
+    return sel;
+}
+
+bool getLegacyChannel(const ProgramSelector& sel, uint32_t& channelOut, uint32_t& subChannelOut) {
+    if (isAmFm(getType(sel))) {
+        channelOut = getId(sel, IdentifierType::AMFM_FREQUENCY);
+        subChannelOut = getId(sel, IdentifierType::HD_SUBCHANNEL, 0);
+        return true;
+    } else {
+        channelOut = 0;
+        subChannelOut = 0;
+        return false;
+    }
+}
+
+bool isDigital(const ProgramSelector& sel) {
+    switch (getType(sel)) {
+        case ProgramType::AM:
+        case ProgramType::FM:
+            return false;
+        default:
+            // VENDOR might not be digital, but it doesn't matter for default impl.
+            return true;
+    }
+}
+
+}  // namespace utils
+}  // namespace implementation
+}  // namespace V1_1
+}  // namespace broadcastradio
+}  // namespace hardware
+}  // namespace android
diff --git a/broadcastradio/1.1/utils/Utils.h b/broadcastradio/1.1/utils/Utils.h
new file mode 100644
index 0000000..1110e79
--- /dev/null
+++ b/broadcastradio/1.1/utils/Utils.h
@@ -0,0 +1,75 @@
+/*
+ * 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.
+ */
+#ifndef ANDROID_HARDWARE_BROADCASTRADIO_V1_1_UTILS_H
+#define ANDROID_HARDWARE_BROADCASTRADIO_V1_1_UTILS_H
+
+#include <android/hardware/broadcastradio/1.1/types.h>
+#include <chrono>
+#include <queue>
+#include <thread>
+
+namespace android {
+namespace hardware {
+namespace broadcastradio {
+namespace V1_1 {
+namespace implementation {
+namespace utils {
+
+/**
+ * Checks, if {@code pointer} tunes to {@channel}.
+ *
+ * For example, having a channel {AMFM_FREQUENCY = 103.3}:
+ * - selector {AMFM_FREQUENCY = 103.3, HD_SUBCHANNEL = 0} can tune to this channel;
+ * - selector {AMFM_FREQUENCY = 103.3, HD_SUBCHANNEL = 1} can't.
+ *
+ * @param pointer selector we're trying to match against channel.
+ * @param channel existing channel.
+ */
+bool tunesTo(const ProgramSelector& pointer, const ProgramSelector& channel);
+
+ProgramType getType(const ProgramSelector& sel);
+bool isAmFm(const ProgramType type);
+
+bool hasId(const ProgramSelector& sel, const IdentifierType type);
+
+/**
+ * Returns ID (either primary or secondary) for a given program selector.
+ *
+ * If the selector does not contain given type, returns 0 and emits a warning.
+ */
+uint64_t getId(const ProgramSelector& sel, const IdentifierType type);
+
+/**
+ * Returns ID (either primary or secondary) for a given program selector.
+ *
+ * If the selector does not contain given type, returns default value.
+ */
+uint64_t getId(const ProgramSelector& sel, const IdentifierType type, uint64_t defval);
+
+ProgramSelector make_selector(V1_0::Band band, uint32_t channel, uint32_t subChannel = 0);
+
+bool getLegacyChannel(const ProgramSelector& sel, uint32_t& channelOut, uint32_t& subChannelOut);
+
+bool isDigital(const ProgramSelector& sel);
+
+}  // namespace utils
+}  // namespace implementation
+}  // namespace V1_1
+}  // namespace broadcastradio
+}  // namespace hardware
+}  // namespace android
+
+#endif  // ANDROID_HARDWARE_BROADCASTRADIO_V1_1_UTILS_H
diff --git a/broadcastradio/1.1/vts/functional/VtsHalBroadcastradioV1_1TargetTest.cpp b/broadcastradio/1.1/vts/functional/VtsHalBroadcastradioV1_1TargetTest.cpp
index b45c8d5..0de6fa5 100644
--- a/broadcastradio/1.1/vts/functional/VtsHalBroadcastradioV1_1TargetTest.cpp
+++ b/broadcastradio/1.1/vts/functional/VtsHalBroadcastradioV1_1TargetTest.cpp
@@ -48,7 +48,7 @@
 using ::android::hardware::broadcastradio::V1_1::ProgramInfo;
 using ::android::hardware::broadcastradio::V1_1::Result;
 using ::android::hardware::broadcastradio::V1_1::ProgramListResult;
-
+using ::android::hardware::hidl_vec;
 
 // The main test class for Broadcast Radio HIDL HAL.
 
@@ -134,7 +134,9 @@
             return Void();
         }
 
-        virtual Return<void> backgroundScanComplete(ProgramListResult result __unused) {
+        virtual Return<void> backgroundScanComplete(ProgramListResult result) {
+            ALOGV("%s", __func__);
+            mParentTest->onProgramListResultCallback(result);
             return Void();
         }
 
@@ -177,6 +179,15 @@
     }
 
     /**
+     * Method called by MyCallback when a callback with status is received
+     */
+    void onProgramListResultCallback(ProgramListResult result) {
+        Mutex::Autolock _l(mLock);
+        mProgramListResultCallbackData = result;
+        onCallback_l();
+    }
+
+    /**
      * Method called by MyCallback when a boolean indication is received
      */
     void onBoolCallback(bool result) {
@@ -221,6 +232,7 @@
 
     static const nsecs_t kConfigCallbacktimeoutNs = seconds_to_nanoseconds(10);
     static const nsecs_t kTuneCallbacktimeoutNs = seconds_to_nanoseconds(30);
+    static const nsecs_t kFullScanTimeoutNs = seconds_to_nanoseconds(60);
 
     sp<IBroadcastRadio> mRadio;
     Properties mHalProperties;
@@ -231,6 +243,7 @@
     bool mCallbackCalled;
     bool mBoolCallbackData;
     Result mResultCallbackData;
+    ProgramListResult mProgramListResultCallbackData;
     bool mHwFailure;
 };
 
@@ -467,6 +480,53 @@
     EXPECT_EQ(Result::OK, hidlResult);
 }
 
+TEST_F(BroadcastRadioHidlTest, TuneFromProgramList) {
+    ASSERT_TRUE(openTuner());
+    ASSERT_TRUE(checkAntenna());
+
+    ProgramInfo firstProgram;
+    bool isListEmpty;
+    ProgramListResult getListResult = ProgramListResult::NOT_INITIALIZED;
+    auto getListCb = [&](ProgramListResult result, const hidl_vec<ProgramInfo>& list) {
+        getListResult = result;
+        if (result != ProgramListResult::OK) return;
+        isListEmpty = (list.size() == 0);
+        // don't copy the whole list out, it might be heavy
+        if (!isListEmpty) firstProgram = list[0];
+    };
+
+    // first try...
+    auto hidlReturn = mTuner->getProgramList("", getListCb);
+    ASSERT_TRUE(hidlReturn.isOk());
+
+    if (getListResult == ProgramListResult::NOT_STARTED) {
+        auto result = mTuner->startBackgroundScan();
+        ASSERT_TRUE(result.isOk());
+        ASSERT_EQ(ProgramListResult::OK, result);
+        getListResult = ProgramListResult::NOT_READY;  // continue as in NOT_READY case
+    }
+    if (getListResult == ProgramListResult::NOT_READY) {
+        ASSERT_TRUE(waitForCallback(kFullScanTimeoutNs));
+        ASSERT_EQ(ProgramListResult::OK, mProgramListResultCallbackData);
+
+        // second (last) try...
+        hidlReturn = mTuner->getProgramList("", getListCb);
+        ASSERT_TRUE(hidlReturn.isOk());
+        ASSERT_EQ(ProgramListResult::OK, getListResult);
+    }
+
+    if (isListEmpty) {
+        std::cout << "[  SKIPPED ] Program list is empty. " << std::endl;
+        return;
+    }
+
+    auto tuneResult = mTuner->tune_1_1(firstProgram.selector);
+    ASSERT_TRUE(tuneResult.isOk());
+    EXPECT_EQ(Result::OK, tuneResult);
+    EXPECT_EQ(true, waitForCallback(kTuneCallbacktimeoutNs));
+    // TODO(b/36864490): check this too, when mProgramInfoCallbackData is cherry-picked from 1.0
+    // EXPECT_EQ(firstProgram.selector.primaryId, mProgramInfoCallbackData.selector.primaryId);
+}
 
 int main(int argc, char** argv) {
   ::testing::AddGlobalTestEnvironment(new BroadcastRadioHidlEnvironment);