Implement program list fetching.

Bug: 69860743
Test: VTS
Change-Id: I04eb43c1e0e1bb7bad86e123594a473454eed983
diff --git a/broadcastradio/2.0/Android.bp b/broadcastradio/2.0/Android.bp
index afbd6d4..1d7861e 100644
--- a/broadcastradio/2.0/Android.bp
+++ b/broadcastradio/2.0/Android.bp
@@ -21,9 +21,11 @@
         "IdentifierType",
         "Metadata",
         "MetadataKey",
+        "ProgramFilter",
         "ProgramIdentifier",
         "ProgramInfo",
         "ProgramInfoFlags",
+        "ProgramListChunk",
         "ProgramSelector",
         "Properties",
         "Result",
diff --git a/broadcastradio/2.0/ITunerCallback.hal b/broadcastradio/2.0/ITunerCallback.hal
index 1aefc4e..ede8350 100644
--- a/broadcastradio/2.0/ITunerCallback.hal
+++ b/broadcastradio/2.0/ITunerCallback.hal
@@ -39,6 +39,21 @@
     oneway onCurrentProgramInfoChanged(ProgramInfo info);
 
     /**
+     * A delta update of the program list, called whenever there's a change in
+     * the list.
+     *
+     * If there are frequent changes, HAL implementation must throttle the rate
+     * of the updates.
+     *
+     * There is a hard limit on binder transaction buffer, and the list must
+     * not exceed it. For large lists, HAL implementation must split them to
+     * multiple chunks, no larger than 500kiB each.
+     *
+     * @param chunk A chunk of the program list update.
+     */
+    oneway onProgramListUpdated(ProgramListChunk chunk);
+
+    /**
      * Method called by the HAL when the antenna gets connected or disconnected.
      *
      * For a new tuner session, client must assume the antenna is connected.
diff --git a/broadcastradio/2.0/ITunerSession.hal b/broadcastradio/2.0/ITunerSession.hal
index 8a21768..a3f93fd 100644
--- a/broadcastradio/2.0/ITunerSession.hal
+++ b/broadcastradio/2.0/ITunerSession.hal
@@ -77,6 +77,32 @@
     cancel();
 
     /**
+     * Applies a filter to the program list and starts sending program list
+     * updates over onProgramListUpdated callback.
+     *
+     * There may be only one updates stream active at the moment. Calling this
+     * method again must result in cancelling the previous update request.
+     *
+     * This call clears the program list on the client side, the HAL must send
+     * the whole list again.
+     *
+     * If the program list scanning hardware (i.e. background tuner) is
+     * unavailable at the moment, the call must succeed and start updates
+     * when it becomes available.
+     *
+     * @param filter Filter to apply on the fetched program list.
+     * @return result OK successfully started fetching list updates.
+     *                NOT_SUPPORTED program list scanning is not supported
+     *                by the hardware.
+     */
+    startProgramListUpdates(ProgramFilter filter) generates (Result result);
+
+    /**
+     * Stops sending program list updates.
+     */
+    stopProgramListUpdates();
+
+    /**
      * Fetches the current setting of a given config flag.
      *
      * The success/failure result must be consistent with setConfigFlag.
diff --git a/broadcastradio/2.0/default/TunerSession.cpp b/broadcastradio/2.0/default/TunerSession.cpp
index 54af3389..36a22c5 100644
--- a/broadcastradio/2.0/default/TunerSession.cpp
+++ b/broadcastradio/2.0/default/TunerSession.cpp
@@ -45,6 +45,7 @@
 static constexpr auto scan = 200ms;
 static constexpr auto step = 100ms;
 static constexpr auto tune = 150ms;
+static constexpr auto list = 1s;
 
 }  // namespace delay
 
@@ -205,6 +206,38 @@
     return {};
 }
 
+Return<Result> TunerSession::startProgramListUpdates(const ProgramFilter& filter) {
+    ALOGV("%s(%s)", __func__, toString(filter).c_str());
+    lock_guard<mutex> lk(mMut);
+    if (mIsClosed) return Result::INVALID_STATE;
+
+    auto list = virtualRadio().getProgramList();
+    vector<VirtualProgram> filteredList;
+    auto filterCb = [&filter](const VirtualProgram& program) {
+        return utils::satisfies(filter, program.selector);
+    };
+    std::copy_if(list.begin(), list.end(), std::back_inserter(filteredList), filterCb);
+
+    auto task = [this, list]() {
+        lock_guard<mutex> lk(mMut);
+
+        ProgramListChunk chunk = {};
+        chunk.purge = true;
+        chunk.complete = true;
+        chunk.modified = hidl_vec<ProgramInfo>(list.begin(), list.end());
+
+        mCallback->onProgramListUpdated(chunk);
+    };
+    mThread.schedule(task, delay::list);
+
+    return Result::OK;
+}
+
+Return<void> TunerSession::stopProgramListUpdates() {
+    ALOGV("%s", __func__);
+    return {};
+}
+
 Return<void> TunerSession::getConfigFlag(ConfigFlag flag, getConfigFlag_cb _hidl_cb) {
     ALOGV("%s(%s)", __func__, toString(flag).c_str());
 
diff --git a/broadcastradio/2.0/default/TunerSession.h b/broadcastradio/2.0/default/TunerSession.h
index 9a72182..a58aa19 100644
--- a/broadcastradio/2.0/default/TunerSession.h
+++ b/broadcastradio/2.0/default/TunerSession.h
@@ -38,6 +38,8 @@
     virtual Return<Result> scan(bool directionUp, bool skipSubChannel) override;
     virtual Return<Result> step(bool directionUp) override;
     virtual Return<void> cancel() override;
+    virtual Return<Result> startProgramListUpdates(const ProgramFilter& filter);
+    virtual Return<void> stopProgramListUpdates();
     virtual Return<void> getConfigFlag(ConfigFlag flag, getConfigFlag_cb _hidl_cb);
     virtual Return<Result> setConfigFlag(ConfigFlag flag, bool value);
     virtual Return<void> setParameters(const hidl_vec<VendorKeyValue>& parameters,
diff --git a/broadcastradio/2.0/types.hal b/broadcastradio/2.0/types.hal
index fc5809f..d50d485 100644
--- a/broadcastradio/2.0/types.hal
+++ b/broadcastradio/2.0/types.hal
@@ -25,6 +25,8 @@
      * onAntennaStateChange callback must be called within this time.
      */
     ANTENNA_DISCONNECTED_TIMEOUT_MS = 100,
+
+    LIST_COMPLETE_TIMEOUT_MS = 300000,
 };
 
 enum Result : int32_t {
@@ -455,6 +457,42 @@
 
     /** Album art (uint32_t, see IBroadcastRadio::getImage) */
     ALBUM_ART,
+
+    /**
+     * Station name.
+     *
+     * This is a generic field to cover any radio technology.
+     *
+     * If the PROGRAM_NAME has the same content as DAB_*_NAME or RDS_PS,
+     * it may not be present, to preserve space - framework must repopulate
+     * it on the client side.
+     */
+    PROGRAM_NAME,
+
+    /** DAB ensemble name (string) */
+    DAB_ENSEMBLE_NAME,
+
+    /**
+     * DAB ensemble name abbreviated (string).
+     *
+     * The string must be up to 8 characters long.
+     *
+     * If the short variant is present, the long (DAB_ENSEMBLE_NAME) one must be
+     * present as well.
+     */
+    DAB_ENSEMBLE_NAME_SHORT,
+
+    /** DAB service name (string) */
+    DAB_SERVICE_NAME,
+
+    /** DAB service name abbreviated (see DAB_ENSEMBLE_NAME_SHORT) (string) */
+    DAB_SERVICE_NAME_SHORT,
+
+    /** DAB component name (string) */
+    DAB_COMPONENT_NAME,
+
+    /** DAB component name abbreviated (see DAB_ENSEMBLE_NAME_SHORT) (string) */
+    DAB_COMPONENT_NAME_SHORT,
 };
 
 /**
@@ -476,3 +514,102 @@
     int64_t intValue;
     string stringValue;
 };
+
+/**
+ * An update packet of the program list.
+ *
+ * The order of entries in the vectors is unspecified.
+ */
+struct ProgramListChunk {
+    /**
+     * Treats all previously added entries as removed.
+     *
+     * This is meant to save binder transaction bandwidth on 'removed' vector
+     * and provide a clear empty state.
+     *
+     * If set, 'removed' vector must be empty.
+     *
+     * The client may wait with taking action on this until it received the
+     * chunk with complete flag set (to avoid part of stations temporarily
+     * disappearing from the list).
+     */
+    bool purge;
+
+    /**
+     * If false, it means there are still programs not transmitted,
+     * due for transmission in following updates.
+     *
+     * Used by UIs that wait for complete list instead of displaying
+     * programs while scanning.
+     *
+     * After the whole channel range was scanned and all discovered programs
+     * were transmitted, the last chunk must have set this flag to true.
+     * This must happen within Constants::LIST_COMPLETE_TIMEOUT_MS from the
+     * startProgramListUpdates call. If it doesn't, client may assume the tuner
+     * came into a bad state and display error message.
+     */
+    bool complete;
+
+    /**
+     * Added or modified program list entries.
+     *
+     * Two entries with the same primaryId (ProgramSelector member)
+     * are considered the same.
+     */
+    vec<ProgramInfo> modified;
+
+    /**
+     * Removed program list entries.
+     *
+     * Contains primaryId (ProgramSelector member) of a program to remove.
+     */
+    vec<ProgramIdentifier> removed;
+};
+
+/**
+ * Large-grain filter to the program list.
+ *
+ * This is meant to reduce binder transaction bandwidth, not for fine-grained
+ * filtering user might expect.
+ *
+ * The filter is designed as conjunctive normal form: the entry that passes the
+ * filter must satisfy all the clauses (members of this struct). Vector clauses
+ * are disjunctions of literals. In other words, there is AND between each
+ * high-level group and OR inside it.
+ */
+struct ProgramFilter {
+    /**
+     * List of identifier types that satisfy the filter.
+     *
+     * If the program list entry contains at least one identifier of the type
+     * listed, it satisfies this condition.
+     *
+     * Empty list means no filtering on identifier type.
+     */
+    vec<uint32_t> identifierTypes;
+
+    /**
+     * List of identifiers that satisfy the filter.
+     *
+     * If the program list entry contains at least one listed identifier,
+     * it satisfies this condition.
+     *
+     * Empty list means no filtering on identifier.
+     */
+    vec<ProgramIdentifier> identifiers;
+
+    /**
+     * Includes non-tunable entries that define tree structure on the
+     * program list (i.e. DAB ensembles).
+     */
+    bool includeCategories;
+
+    /**
+     * Disable updates on entry modifications.
+     *
+     * If true, 'modified' vector of ProgramListChunk must contain list
+     * additions only. Once the program is added to the list, it's not
+     * updated anymore.
+     */
+    bool excludeModifications;
+};
diff --git a/broadcastradio/2.0/vts/functional/VtsHalBroadcastradioV2_0TargetTest.cpp b/broadcastradio/2.0/vts/functional/VtsHalBroadcastradioV2_0TargetTest.cpp
index d0e4144..46b3f19 100644
--- a/broadcastradio/2.0/vts/functional/VtsHalBroadcastradioV2_0TargetTest.cpp
+++ b/broadcastradio/2.0/vts/functional/VtsHalBroadcastradioV2_0TargetTest.cpp
@@ -38,6 +38,7 @@
 
 using namespace std::chrono_literals;
 
+using std::unordered_set;
 using std::vector;
 using testing::_;
 using testing::AnyNumber;
@@ -54,6 +55,7 @@
 namespace timeout {
 
 static constexpr auto tune = 30s;
+static constexpr auto programListScan = 5min;
 
 }  // namespace timeout
 
@@ -63,16 +65,20 @@
     ConfigFlag::DAB_HARD_LINKING, ConfigFlag::DAB_SOFT_LINKING,
 };
 
-struct TunerCallbackMock : public ITunerCallback {
-    TunerCallbackMock() {
-        // we expect the antenna is connected through the whole test
-        EXPECT_CALL(*this, onAntennaStateChange(false)).Times(0);
-    }
+class TunerCallbackMock : public ITunerCallback {
+   public:
+    TunerCallbackMock();
 
     MOCK_METHOD2(onTuneFailed, Return<void>(Result, const ProgramSelector&));
     MOCK_TIMEOUT_METHOD1(onCurrentProgramInfoChanged, Return<void>(const ProgramInfo&));
+    Return<void> onProgramListUpdated(const ProgramListChunk& chunk);
     MOCK_METHOD1(onAntennaStateChange, Return<void>(bool connected));
     MOCK_METHOD1(onParametersUpdated, Return<void>(const hidl_vec<VendorKeyValue>& parameters));
+
+    MOCK_TIMEOUT_METHOD0(onProgramListReady, void());
+
+    std::mutex mLock;
+    utils::ProgramInfoSet mProgramList;
 };
 
 class BroadcastRadioHalTest : public ::testing::VtsHalHidlTargetTestBase {
@@ -88,6 +94,25 @@
     sp<TunerCallbackMock> mCallback = new TunerCallbackMock();
 };
 
+static void printSkipped(std::string msg) {
+    std::cout << "[  SKIPPED ] " << msg << std::endl;
+}
+
+TunerCallbackMock::TunerCallbackMock() {
+    // we expect the antenna is connected through the whole test
+    EXPECT_CALL(*this, onAntennaStateChange(false)).Times(0);
+}
+
+Return<void> TunerCallbackMock::onProgramListUpdated(const ProgramListChunk& chunk) {
+    std::lock_guard<std::mutex> lk(mLock);
+
+    updateProgramList(mProgramList, chunk);
+
+    if (chunk.complete) onProgramListReady();
+
+    return {};
+}
+
 void BroadcastRadioHalTest::SetUp() {
     EXPECT_EQ(nullptr, mModule.get()) << "Module is already open";
 
@@ -463,6 +488,32 @@
     }
 }
 
+/**
+ * Test getting program list.
+ *
+ * Verifies that:
+ * - startProgramListUpdates either succeeds or returns NOT_SUPPORTED;
+ * - the complete list is fetched within timeout::programListScan;
+ * - stopProgramListUpdates does not crash.
+ */
+TEST_F(BroadcastRadioHalTest, GetProgramList) {
+    ASSERT_TRUE(openSession());
+
+    EXPECT_TIMEOUT_CALL(*mCallback, onProgramListReady).Times(AnyNumber());
+
+    auto startResult = mSession->startProgramListUpdates({});
+    if (startResult == Result::NOT_SUPPORTED) {
+        printSkipped("Program list not supported");
+        return;
+    }
+    ASSERT_EQ(Result::OK, startResult);
+
+    EXPECT_TIMEOUT_CALL_WAIT(*mCallback, onProgramListReady, timeout::programListScan);
+
+    auto stopResult = mSession->stopProgramListUpdates();
+    EXPECT_TRUE(stopResult.isOk());
+}
+
 }  // namespace vts
 }  // namespace V2_0
 }  // namespace broadcastradio
diff --git a/broadcastradio/common/tests/Android.bp b/broadcastradio/common/tests/Android.bp
index bbad527..512c02e 100644
--- a/broadcastradio/common/tests/Android.bp
+++ b/broadcastradio/common/tests/Android.bp
@@ -15,20 +15,6 @@
 //
 
 cc_test {
-    name: "android.hardware.broadcastradio@common-utils-tests",
-    vendor: true,
-    cflags: [
-        "-Wall",
-        "-Wextra",
-        "-Werror",
-    ],
-    srcs: [
-        "WorkerThread_test.cpp",
-    ],
-    static_libs: ["android.hardware.broadcastradio@common-utils-lib"],
-}
-
-cc_test {
     name: "android.hardware.broadcastradio@common-utils-xx-tests",
     vendor: true,
     cflags: [
@@ -48,3 +34,36 @@
         "[email protected]",
     ],
 }
+
+cc_test {
+    name: "android.hardware.broadcastradio@common-utils-2x-tests",
+    vendor: true,
+    cflags: [
+        "-Wall",
+        "-Wextra",
+        "-Werror",
+    ],
+    srcs: [
+        "IdentifierIterator_test.cpp",
+    ],
+    static_libs: [
+        "android.hardware.broadcastradio@common-utils-2x-lib",
+    ],
+    shared_libs: [
+        "[email protected]",
+    ],
+}
+
+cc_test {
+    name: "android.hardware.broadcastradio@common-utils-tests",
+    vendor: true,
+    cflags: [
+        "-Wall",
+        "-Wextra",
+        "-Werror",
+    ],
+    srcs: [
+        "WorkerThread_test.cpp",
+    ],
+    static_libs: ["android.hardware.broadcastradio@common-utils-lib"],
+}
diff --git a/broadcastradio/common/tests/IdentifierIterator_test.cpp b/broadcastradio/common/tests/IdentifierIterator_test.cpp
new file mode 100644
index 0000000..5bf222b
--- /dev/null
+++ b/broadcastradio/common/tests/IdentifierIterator_test.cpp
@@ -0,0 +1,123 @@
+/*
+ * 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.
+ */
+
+#include <broadcastradio-utils-2x/Utils.h>
+#include <gtest/gtest.h>
+
+namespace {
+
+namespace V2_0 = android::hardware::broadcastradio::V2_0;
+namespace utils = android::hardware::broadcastradio::utils;
+
+using V2_0::IdentifierType;
+using V2_0::ProgramSelector;
+
+TEST(IdentifierIteratorTest, singleSecondary) {
+    // clang-format off
+    V2_0::ProgramSelector sel {
+        utils::make_identifier(IdentifierType::RDS_PI, 0xBEEF),
+        {utils::make_identifier(IdentifierType::AMFM_FREQUENCY, 100100)}
+    };
+    // clang-format on
+
+    auto it = utils::begin(sel);
+    auto end = utils::end(sel);
+
+    ASSERT_NE(end, it);
+    EXPECT_EQ(sel.primaryId, *it);
+    ASSERT_NE(end, ++it);
+    EXPECT_EQ(sel.secondaryIds[0], *it);
+    ASSERT_EQ(end, ++it);
+}
+
+TEST(IdentifierIteratorTest, empty) {
+    V2_0::ProgramSelector sel{};
+
+    auto it = utils::begin(sel);
+    auto end = utils::end(sel);
+
+    ASSERT_NE(end, it++);  // primary id is always present
+    ASSERT_EQ(end, it);
+}
+
+TEST(IdentifierIteratorTest, twoSelectors) {
+    V2_0::ProgramSelector sel1{};
+    V2_0::ProgramSelector sel2{};
+
+    auto it1 = utils::begin(sel1);
+    auto it2 = utils::begin(sel2);
+
+    EXPECT_NE(it1, it2);
+}
+
+TEST(IdentifierIteratorTest, increments) {
+    V2_0::ProgramSelector sel{{}, {{}, {}}};
+
+    auto it = utils::begin(sel);
+    auto end = utils::end(sel);
+    auto pre = it;
+    auto post = it;
+
+    EXPECT_NE(++pre, post++);
+    EXPECT_EQ(pre, post);
+    EXPECT_EQ(pre, it + 1);
+    ASSERT_NE(end, pre);
+}
+
+TEST(IdentifierIteratorTest, findType) {
+    using namespace std::placeholders;
+
+    uint64_t rds_pi1 = 0xDEAD;
+    uint64_t rds_pi2 = 0xBEEF;
+    uint64_t freq1 = 100100;
+    uint64_t freq2 = 107900;
+
+    // clang-format off
+    V2_0::ProgramSelector sel {
+        utils::make_identifier(IdentifierType::RDS_PI, rds_pi1),
+        {
+            utils::make_identifier(IdentifierType::AMFM_FREQUENCY, freq1),
+            utils::make_identifier(IdentifierType::RDS_PI, rds_pi2),
+            utils::make_identifier(IdentifierType::AMFM_FREQUENCY, freq2),
+        }
+    };
+    // clang-format on
+
+    auto typeEquals = [](const V2_0::ProgramIdentifier& id, V2_0::IdentifierType type) {
+        return utils::getType(id) == type;
+    };
+    auto isRdsPi = std::bind(typeEquals, _1, IdentifierType::RDS_PI);
+    auto isFreq = std::bind(typeEquals, _1, IdentifierType::AMFM_FREQUENCY);
+
+    auto end = utils::end(sel);
+    auto it = std::find_if(utils::begin(sel), end, isRdsPi);
+    ASSERT_NE(end, it);
+    EXPECT_EQ(rds_pi1, it->value);
+
+    it = std::find_if(it + 1, end, isRdsPi);
+    ASSERT_NE(end, it);
+    EXPECT_EQ(rds_pi2, it->value);
+
+    it = std::find_if(utils::begin(sel), end, isFreq);
+    ASSERT_NE(end, it);
+    EXPECT_EQ(freq1, it->value);
+
+    it = std::find_if(++it, end, isFreq);
+    ASSERT_NE(end, it);
+    EXPECT_EQ(freq2, it->value);
+}
+
+}  // anonymous namespace
diff --git a/broadcastradio/common/utils2x/Utils.cpp b/broadcastradio/common/utils2x/Utils.cpp
index d157108..10a155b 100644
--- a/broadcastradio/common/utils2x/Utils.cpp
+++ b/broadcastradio/common/utils2x/Utils.cpp
@@ -18,6 +18,7 @@
 
 #include <broadcastradio-utils-2x/Utils.h>
 
+#include <android-base/logging.h>
 #include <log/log.h>
 
 namespace android {
@@ -28,14 +29,64 @@
 using V2_0::IdentifierType;
 using V2_0::Metadata;
 using V2_0::MetadataKey;
+using V2_0::ProgramFilter;
 using V2_0::ProgramIdentifier;
+using V2_0::ProgramInfo;
+using V2_0::ProgramListChunk;
 using V2_0::ProgramSelector;
+using V2_0::Properties;
 
 using std::string;
 using std::vector;
 
+IdentifierType getType(uint32_t typeAsInt) {
+    return static_cast<IdentifierType>(typeAsInt);
+}
+
 IdentifierType getType(const ProgramIdentifier& id) {
-    return static_cast<IdentifierType>(id.type);
+    return getType(id.type);
+}
+
+IdentifierIterator::IdentifierIterator(const V2_0::ProgramSelector& sel)
+    : IdentifierIterator(sel, 0) {}
+
+IdentifierIterator::IdentifierIterator(const V2_0::ProgramSelector& sel, size_t pos)
+    : mSel(sel), mPos(pos) {}
+
+IdentifierIterator IdentifierIterator::operator++(int) {
+    auto i = *this;
+    mPos++;
+    return i;
+}
+
+IdentifierIterator& IdentifierIterator::operator++() {
+    ++mPos;
+    return *this;
+}
+
+IdentifierIterator::ref_type IdentifierIterator::operator*() const {
+    if (mPos == 0) return sel().primaryId;
+
+    // mPos is 1-based for secondary identifiers
+    DCHECK(mPos <= sel().secondaryIds.size());
+    return sel().secondaryIds[mPos - 1];
+}
+
+bool IdentifierIterator::operator==(const IdentifierIterator& rhs) const {
+    // Check, if both iterators points at the same selector.
+    if (reinterpret_cast<uintptr_t>(&sel()) != reinterpret_cast<uintptr_t>(&rhs.sel())) {
+        return false;
+    }
+
+    return mPos == rhs.mPos;
+}
+
+IdentifierIterator begin(const V2_0::ProgramSelector& sel) {
+    return IdentifierIterator(sel);
+}
+
+IdentifierIterator end(const V2_0::ProgramSelector& sel) {
+    return IdentifierIterator(sel) + 1 /* primary id */ + sel.secondaryIds.size();
 }
 
 static bool bothHaveId(const ProgramSelector& a, const ProgramSelector& b,
@@ -88,6 +139,7 @@
         return true;
     }
 
+    // TODO(twasilczyk): use IdentifierIterator
     // not optimal, but we don't care in default impl
     for (auto&& id : sel.secondaryIds) {
         if (id.type == itype) {
@@ -125,6 +177,7 @@
 
     if (sel.primaryId.type == itype) ret.push_back(sel.primaryId.value);
 
+    // TODO(twasilczyk): use IdentifierIterator
     for (auto&& id : sel.secondaryIds) {
         if (id.type == itype) ret.push_back(id.value);
     }
@@ -132,11 +185,11 @@
     return ret;
 }
 
-bool isSupported(const V2_0::Properties& prop, const V2_0::ProgramSelector& sel) {
+bool isSupported(const Properties& prop, const ProgramSelector& sel) {
+    // TODO(twasilczyk): use IdentifierIterator
     // Not optimal, but it doesn't matter for default impl nor VTS tests.
-    for (auto&& idTypeI : prop.supportedIdentifierTypes) {
-        auto idType = static_cast<IdentifierType>(idTypeI);
-        if (hasId(sel, idType)) return true;
+    for (auto&& idType : prop.supportedIdentifierTypes) {
+        if (hasId(sel, getType(idType))) return true;
     }
     return false;
 }
@@ -152,7 +205,7 @@
         }
     };
 
-    switch (static_cast<IdentifierType>(id.type)) {
+    switch (getType(id)) {
         case IdentifierType::AMFM_FREQUENCY:
         case IdentifierType::DAB_FREQUENCY:
         case IdentifierType::DRMO_FREQUENCY:
@@ -211,8 +264,9 @@
     return valid;
 }
 
-bool isValid(const V2_0::ProgramSelector& sel) {
+bool isValid(const ProgramSelector& sel) {
     if (!isValid(sel.primaryId)) return false;
+    // TODO(twasilczyk): use IdentifierIterator
     for (auto&& id : sel.secondaryIds) {
         if (!isValid(id)) return false;
     }
@@ -243,6 +297,59 @@
     return meta;
 }
 
+bool satisfies(const ProgramFilter& filter, const ProgramSelector& sel) {
+    if (filter.identifierTypes.size() > 0) {
+        auto typeEquals = [](const V2_0::ProgramIdentifier& id, uint32_t type) {
+            return id.type == type;
+        };
+        auto it = std::find_first_of(begin(sel), end(sel), filter.identifierTypes.begin(),
+                                     filter.identifierTypes.end(), typeEquals);
+        if (it == end(sel)) return false;
+    }
+
+    if (filter.identifiers.size() > 0) {
+        auto it = std::find_first_of(begin(sel), end(sel), filter.identifiers.begin(),
+                                     filter.identifiers.end());
+        if (it == end(sel)) return false;
+    }
+
+    if (!filter.includeCategories) {
+        if (getType(sel.primaryId) == IdentifierType::DAB_ENSEMBLE) return false;
+    }
+
+    return true;
+}
+
+size_t ProgramInfoHasher::operator()(const ProgramInfo& info) const {
+    auto& id = info.selector.primaryId;
+
+    /* This is not the best hash implementation, but good enough for default HAL
+     * implementation and tests. */
+    auto h = std::hash<uint32_t>{}(id.type);
+    h += 0x9e3779b9;
+    h ^= std::hash<uint64_t>{}(id.value);
+
+    return h;
+}
+
+bool ProgramInfoKeyEqual::operator()(const ProgramInfo& info1, const ProgramInfo& info2) const {
+    auto& id1 = info1.selector.primaryId;
+    auto& id2 = info2.selector.primaryId;
+    return id1.type == id2.type && id1.value == id2.value;
+}
+
+void updateProgramList(ProgramInfoSet& list, const ProgramListChunk& chunk) {
+    if (chunk.purge) list.clear();
+
+    list.insert(chunk.modified.begin(), chunk.modified.end());
+
+    for (auto&& id : chunk.removed) {
+        ProgramInfo info = {};
+        info.selector.primaryId = id;
+        list.erase(info);
+    }
+}
+
 }  // namespace utils
 }  // namespace broadcastradio
 }  // namespace hardware
diff --git a/broadcastradio/common/utils2x/include/broadcastradio-utils-2x/Utils.h b/broadcastradio/common/utils2x/include/broadcastradio-utils-2x/Utils.h
index dd01852..bac11fd 100644
--- a/broadcastradio/common/utils2x/include/broadcastradio-utils-2x/Utils.h
+++ b/broadcastradio/common/utils2x/include/broadcastradio-utils-2x/Utils.h
@@ -20,14 +20,49 @@
 #include <chrono>
 #include <queue>
 #include <thread>
+#include <unordered_set>
 
 namespace android {
 namespace hardware {
 namespace broadcastradio {
 namespace utils {
 
+V2_0::IdentifierType getType(uint32_t typeAsInt);
 V2_0::IdentifierType getType(const V2_0::ProgramIdentifier& id);
 
+class IdentifierIterator
+    : public std::iterator<std::random_access_iterator_tag, V2_0::ProgramIdentifier, ssize_t,
+                           const V2_0::ProgramIdentifier*, const V2_0::ProgramIdentifier&> {
+    using traits = std::iterator_traits<IdentifierIterator>;
+    using ptr_type = typename traits::pointer;
+    using ref_type = typename traits::reference;
+    using diff_type = typename traits::difference_type;
+
+   public:
+    explicit IdentifierIterator(const V2_0::ProgramSelector& sel);
+
+    IdentifierIterator operator++(int);
+    IdentifierIterator& operator++();
+    ref_type operator*() const;
+    inline ptr_type operator->() const { return &operator*(); }
+    IdentifierIterator operator+(diff_type v) const { return IdentifierIterator(mSel, mPos + v); }
+    bool operator==(const IdentifierIterator& rhs) const;
+    inline bool operator!=(const IdentifierIterator& rhs) const { return !operator==(rhs); };
+
+   private:
+    explicit IdentifierIterator(const V2_0::ProgramSelector& sel, size_t pos);
+
+    std::reference_wrapper<const V2_0::ProgramSelector> mSel;
+
+    const V2_0::ProgramSelector& sel() const { return mSel.get(); }
+
+    /** 0 is the primary identifier, 1-n are secondary identifiers. */
+    size_t mPos = 0;
+};
+
+IdentifierIterator begin(const V2_0::ProgramSelector& sel);
+IdentifierIterator end(const V2_0::ProgramSelector& sel);
+
 /**
  * Checks, if {@code pointer} tunes to {@channel}.
  *
@@ -77,6 +112,21 @@
 V2_0::Metadata make_metadata(V2_0::MetadataKey key, int64_t value);
 V2_0::Metadata make_metadata(V2_0::MetadataKey key, std::string value);
 
+bool satisfies(const V2_0::ProgramFilter& filter, const V2_0::ProgramSelector& sel);
+
+struct ProgramInfoHasher {
+    size_t operator()(const V2_0::ProgramInfo& info) const;
+};
+
+struct ProgramInfoKeyEqual {
+    bool operator()(const V2_0::ProgramInfo& info1, const V2_0::ProgramInfo& info2) const;
+};
+
+typedef std::unordered_set<V2_0::ProgramInfo, ProgramInfoHasher, ProgramInfoKeyEqual>
+    ProgramInfoSet;
+
+void updateProgramList(ProgramInfoSet& list, const V2_0::ProgramListChunk& chunk);
+
 }  // namespace utils
 }  // namespace broadcastradio
 }  // namespace hardware
diff --git a/broadcastradio/common/vts/utils/include/broadcastradio-vts-utils/mock-timeout.h b/broadcastradio/common/vts/utils/include/broadcastradio-vts-utils/mock-timeout.h
index b0ce088..12453bb 100644
--- a/broadcastradio/common/vts/utils/include/broadcastradio-vts-utils/mock-timeout.h
+++ b/broadcastradio/common/vts/utils/include/broadcastradio-vts-utils/mock-timeout.h
@@ -30,18 +30,41 @@
     std::condition_variable egmock_cond_##Method;
 
 /**
+ * Function similar to comma operator, to make it possible to return any value returned by mocked
+ * function (which may be void) and discard the result of the other operation (notification about
+ * a call).
+ *
+ * We need to invoke the mocked function (which result is returned) before the notification (which
+ * result is dropped) - that's exactly the opposite of comma operator.
+ *
+ * INTERNAL IMPLEMENTATION - don't use in user code.
+ */
+template <typename T>
+static T EGMockFlippedComma_(std::function<T()> returned, std::function<void()> discarded) {
+    auto ret = returned();
+    discarded();
+    return ret;
+}
+
+template <>
+inline void EGMockFlippedComma_(std::function<void()> returned, std::function<void()> discarded) {
+    returned();
+    discarded();
+}
+
+/**
  * Common method body for gmock timeout extension.
  *
  * INTERNAL IMPLEMENTATION - don't use in user code.
  */
-#define EGMOCK_TIMEOUT_METHOD_BODY_(Method, ...)             \
-    auto ret = egmock_##Method(__VA_ARGS__);                 \
-    {                                                        \
-        std::lock_guard<std::mutex> lk(egmock_mut_##Method); \
-        egmock_called_##Method = true;                       \
-        egmock_cond_##Method.notify_all();                   \
-    }                                                        \
-    return ret;
+#define EGMOCK_TIMEOUT_METHOD_BODY_(Method, ...)                      \
+    auto invokeMock = [&]() { return egmock_##Method(__VA_ARGS__); }; \
+    auto notify = [&]() {                                             \
+        std::lock_guard<std::mutex> lk(egmock_mut_##Method);          \
+        egmock_called_##Method = true;                                \
+        egmock_cond_##Method.notify_all();                            \
+    };                                                                \
+    return EGMockFlippedComma_<decltype(invokeMock())>(invokeMock, notify);
 
 /**
  * Gmock MOCK_METHOD0 timeout-capable extension.