Implement regional configuration fetching.

Bug: 69958423
Test: VTS
Change-Id: I7c184191b4f4999bd03b06bd3b2283e028694918
diff --git a/broadcastradio/2.0/Android.bp b/broadcastradio/2.0/Android.bp
index 1d7861e..f4894ad 100644
--- a/broadcastradio/2.0/Android.bp
+++ b/broadcastradio/2.0/Android.bp
@@ -16,8 +16,12 @@
         "android.hidl.base@1.0",
     ],
     types: [
+        "AmFmBandRange",
+        "AmFmRegionConfig",
         "ConfigFlag",
         "Constants",
+        "DabTableEntry",
+        "Deemphasis",
         "IdentifierType",
         "Metadata",
         "MetadataKey",
@@ -28,6 +32,7 @@
         "ProgramListChunk",
         "ProgramSelector",
         "Properties",
+        "Rds",
         "Result",
         "VendorKeyValue",
     ],
diff --git a/broadcastradio/2.0/IBroadcastRadio.hal b/broadcastradio/2.0/IBroadcastRadio.hal
index 3ab1cc2..3b19e61 100644
--- a/broadcastradio/2.0/IBroadcastRadio.hal
+++ b/broadcastradio/2.0/IBroadcastRadio.hal
@@ -33,6 +33,28 @@
     getProperties() generates (Properties properties);
 
     /**
+     * Fetches current or possible AM/FM region configuration.
+     *
+     * @param full If true, returns full hardware capabilities.
+     *             If false, returns current regional configuration.
+     * @return result OK in case of success.
+     *                NOT_SUPPORTED if the tuner doesn't support AM/FM.
+     * @return config Hardware capabilities (full=true) or
+     *                current configuration (full=false).
+     */
+    getAmFmRegionConfig(bool full)
+            generates (Result result, AmFmRegionConfig config);
+
+    /**
+     * Fetches current DAB region configuration.
+     *
+     * @return result OK in case of success.
+     *                NOT_SUPPORTED if the tuner doesn't support DAB.
+     * @return config Current configuration.
+     */
+    getDabRegionConfig() generates (Result result, vec<DabTableEntry> config);
+
+    /**
      * Opens a new tuner session.
      *
      * There may be only one session active at a time. If the new session was
diff --git a/broadcastradio/2.0/default/Android.bp b/broadcastradio/2.0/default/Android.bp
index 6d4effb..900454e 100644
--- a/broadcastradio/2.0/default/Android.bp
+++ b/broadcastradio/2.0/default/Android.bp
@@ -24,6 +24,9 @@
         "-Wextra",
         "-Werror",
     ],
+    cppflags: [
+        "-std=c++1z",
+    ],
     srcs: [
         "BroadcastRadio.cpp",
         "TunerSession.cpp",
diff --git a/broadcastradio/2.0/default/BroadcastRadio.cpp b/broadcastradio/2.0/default/BroadcastRadio.cpp
index 5ab517d..5dde8a7 100644
--- a/broadcastradio/2.0/default/BroadcastRadio.cpp
+++ b/broadcastradio/2.0/default/BroadcastRadio.cpp
@@ -33,6 +33,16 @@
 using std::mutex;
 using std::vector;
 
+static const AmFmRegionConfig gDefaultAmFmConfig = {  //
+    {
+        {87500, 108000, 100, 100},  // FM
+        {153, 282, 3, 9},           // AM LW
+        {531, 1620, 9, 9},          // AM MW
+        {1600, 30000, 1, 5},        // AM SW
+    },
+    static_cast<uint32_t>(Deemphasis::D50),
+    static_cast<uint32_t>(Rds::RDS)};
+
 static Properties initProperties(const VirtualRadio& virtualRadio) {
     Properties prop = {};
 
@@ -51,7 +61,9 @@
 }
 
 BroadcastRadio::BroadcastRadio(const VirtualRadio& virtualRadio)
-    : mVirtualRadio(virtualRadio), mProperties(initProperties(virtualRadio)) {}
+    : mVirtualRadio(virtualRadio),
+      mProperties(initProperties(virtualRadio)),
+      mAmFmConfig(gDefaultAmFmConfig) {}
 
 Return<void> BroadcastRadio::getProperties(getProperties_cb _hidl_cb) {
     ALOGV("%s", __func__);
@@ -59,6 +71,44 @@
     return {};
 }
 
+AmFmRegionConfig BroadcastRadio::getAmFmConfig() const {
+    lock_guard<mutex> lk(mMut);
+    return mAmFmConfig;
+}
+
+Return<void> BroadcastRadio::getAmFmRegionConfig(bool full, getAmFmRegionConfig_cb _hidl_cb) {
+    ALOGV("%s(%d)", __func__, full);
+
+    if (full) {
+        AmFmRegionConfig config = {};
+        config.ranges = hidl_vec<AmFmBandRange>({
+            {65000, 108000, 10, 0},  // FM
+            {150, 30000, 1, 0},      // AM
+        });
+        config.fmDeemphasis = Deemphasis::D50 | Deemphasis::D75;
+        config.fmRds = Rds::RDS | Rds::RBDS;
+        _hidl_cb(Result::OK, config);
+        return {};
+    } else {
+        _hidl_cb(Result::OK, getAmFmConfig());
+        return {};
+    }
+}
+
+Return<void> BroadcastRadio::getDabRegionConfig(getDabRegionConfig_cb _hidl_cb) {
+    ALOGV("%s", __func__);
+
+    hidl_vec<DabTableEntry> config = {
+        {"5A", 174928},  {"7D", 194064},  {"8A", 195936},  {"8B", 197648},  {"9A", 202928},
+        {"9B", 204640},  {"9C", 206352},  {"10B", 211648}, {"10C", 213360}, {"10D", 215072},
+        {"11A", 216928}, {"11B", 218640}, {"11C", 220352}, {"11D", 222064}, {"12A", 223936},
+        {"12B", 225648}, {"12C", 227360}, {"12D", 229072},
+    };
+
+    _hidl_cb(Result::OK, config);
+    return {};
+}
+
 Return<void> BroadcastRadio::openSession(const sp<ITunerCallback>& callback,
                                          openSession_cb _hidl_cb) {
     ALOGV("%s", __func__);
diff --git a/broadcastradio/2.0/default/BroadcastRadio.h b/broadcastradio/2.0/default/BroadcastRadio.h
index fcf0615..733cadf 100644
--- a/broadcastradio/2.0/default/BroadcastRadio.h
+++ b/broadcastradio/2.0/default/BroadcastRadio.h
@@ -32,14 +32,19 @@
 
     // V2_0::IBroadcastRadio methods
     Return<void> getProperties(getProperties_cb _hidl_cb) override;
+    Return<void> getAmFmRegionConfig(bool full, getAmFmRegionConfig_cb _hidl_cb);
+    Return<void> getDabRegionConfig(getDabRegionConfig_cb _hidl_cb);
     Return<void> openSession(const sp<ITunerCallback>& callback, openSession_cb _hidl_cb) override;
     Return<void> getImage(uint32_t id, getImage_cb _hidl_cb);
 
     std::reference_wrapper<const VirtualRadio> mVirtualRadio;
     Properties mProperties;
 
+    AmFmRegionConfig getAmFmConfig() const;
+
    private:
-    std::mutex mMut;
+    mutable std::mutex mMut;
+    AmFmRegionConfig mAmFmConfig;
     wp<TunerSession> mSession;
 };
 
diff --git a/broadcastradio/2.0/default/TunerSession.cpp b/broadcastradio/2.0/default/TunerSession.cpp
index 244544a..3166d86 100644
--- a/broadcastradio/2.0/default/TunerSession.cpp
+++ b/broadcastradio/2.0/default/TunerSession.cpp
@@ -77,8 +77,12 @@
     mCallback->onCurrentProgramInfoChanged(programInfo);
 }
 
+const BroadcastRadio& TunerSession::module() const {
+    return mModule.get();
+}
+
 const VirtualRadio& TunerSession::virtualRadio() const {
-    return mModule.get().mVirtualRadio;
+    return module().mVirtualRadio;
 }
 
 Return<Result> TunerSession::tune(const ProgramSelector& sel) {
@@ -86,7 +90,7 @@
     lock_guard<mutex> lk(mMut);
     if (mIsClosed) return Result::INVALID_STATE;
 
-    if (!utils::isSupported(mModule.get().mProperties, sel)) {
+    if (!utils::isSupported(module().mProperties, sel)) {
         ALOGW("Selector not supported");
         return Result::NOT_SUPPORTED;
     }
@@ -170,23 +174,19 @@
     mIsTuneCompleted = false;
 
     auto stepTo = utils::getId(mCurrentProgram, IdentifierType::AMFM_FREQUENCY);
-#if 0
-    // TODO(b/69958423): handle regions
-    if (directionUp) {
-        stepTo += mAmfmConfig.spacings[0];
-    } else {
-        stepTo -= mAmfmConfig.spacings[0];
+    auto range = getAmFmRangeLocked();
+    if (!range) {
+        ALOGE("Can't find current band");
+        return Result::INTERNAL_ERROR;
     }
 
-    if (stepTo > mAmfmConfig.upperLimit) stepTo = mAmfmConfig.lowerLimit;
-    if (stepTo < mAmfmConfig.lowerLimit) stepTo = mAmfmConfig.upperLimit;
-#else
     if (directionUp) {
-        stepTo += 100;
+        stepTo += range->spacing;
     } else {
-        stepTo -= 100;
+        stepTo -= range->spacing;
     }
-#endif
+    if (stepTo > range->upperBound) stepTo = range->lowerBound;
+    if (stepTo < range->lowerBound) stepTo = range->upperBound;
 
     auto task = [this, stepTo]() {
         ALOGI("Performing step to %s", std::to_string(stepTo).c_str());
@@ -280,6 +280,18 @@
     return {};
 }
 
+std::optional<AmFmBandRange> TunerSession::getAmFmRangeLocked() const {
+    if (!mIsTuneCompleted) return {};
+    if (!utils::hasId(mCurrentProgram, IdentifierType::AMFM_FREQUENCY)) return {};
+
+    auto freq = utils::getId(mCurrentProgram, IdentifierType::AMFM_FREQUENCY);
+    for (auto&& range : module().getAmFmConfig().ranges) {
+        if (range.lowerBound <= freq && range.upperBound >= freq) return range;
+    }
+
+    return {};
+}
+
 }  // namespace implementation
 }  // namespace V2_0
 }  // namespace broadcastradio
diff --git a/broadcastradio/2.0/default/TunerSession.h b/broadcastradio/2.0/default/TunerSession.h
index a58aa19..5d27b1e 100644
--- a/broadcastradio/2.0/default/TunerSession.h
+++ b/broadcastradio/2.0/default/TunerSession.h
@@ -22,6 +22,8 @@
 #include <android/hardware/broadcastradio/2.0/ITunerSession.h>
 #include <broadcastradio-utils/WorkerThread.h>
 
+#include <optional>
+
 namespace android {
 namespace hardware {
 namespace broadcastradio {
@@ -48,6 +50,8 @@
                                        getParameters_cb _hidl_cb) override;
     virtual Return<void> close() override;
 
+    std::optional<AmFmBandRange> getAmFmRangeLocked() const;
+
    private:
     std::mutex mMut;
     WorkerThread mThread;
@@ -61,6 +65,7 @@
 
     void tuneInternalLocked(const ProgramSelector& sel);
     const VirtualRadio& virtualRadio() const;
+    const BroadcastRadio& module() const;
 };
 
 }  // namespace implementation
diff --git a/broadcastradio/2.0/types.hal b/broadcastradio/2.0/types.hal
index b5264f4..38a5709 100644
--- a/broadcastradio/2.0/types.hal
+++ b/broadcastradio/2.0/types.hal
@@ -32,6 +32,7 @@
 enum Result : int32_t {
     OK,
     UNKNOWN_ERROR,
+    INTERNAL_ERROR,
     INVALID_ARGUMENTS,
     INVALID_STATE,
     NOT_SUPPORTED,
@@ -120,6 +121,108 @@
 };
 
 /**
+ * A supported or configured RDS variant.
+ *
+ * Both might be set for hardware capabilities check (with full=true when
+ * calling getAmFmRegionConfig), but only one (or none) for specific
+ * region settings.
+ */
+enum Rds : uint8_t {
+    /** Standard variant, used everywhere except North America. */
+    RDS = 1 << 0,
+
+    /** Variant used in North America. */
+    RBDS = 1 << 1,
+};
+
+/**
+ * FM de-emphasis filter supported or configured.
+ *
+ * Both might be set for hardware capabilities check (with full=true when
+ * calling getAmFmRegionConfig), but exactly one for specific region settings.
+ */
+enum Deemphasis : uint8_t {
+    D50 = 1 << 0,
+    D75 = 1 << 1,
+};
+
+/**
+ * Regional configuration for AM/FM.
+ *
+ * For hardware capabilities check (with full=true when calling
+ * getAmFmRegionConfig), HAL implementation fills entire supported range of
+ * frequencies and features.
+ *
+ * When checking current configuration, at most one bit in each bitfield
+ * can be set.
+ */
+struct AmFmRegionConfig {
+    /**
+     * All supported or configured AM/FM bands.
+     *
+     * AM/FM bands are identified by frequency value
+     * (see IdentifierType::AMFM_FREQUENCY).
+     *
+     * With typical configuration, it's expected to have two frequency ranges
+     * for capabilities check (AM and FM) and four ranges for specific region
+     * configuration (AM LW, AM MW, AM SW, FM).
+     */
+    vec<AmFmBandRange> ranges;
+
+    /** De-emphasis filter supported/configured. */
+    bitfield<Deemphasis> fmDeemphasis;
+
+    /** RDS/RBDS variant supported/configured. */
+    bitfield<Rds> fmRds;
+};
+
+/**
+ * AM/FM band range for region configuration.
+ *
+ * Defines channel grid: each possible channel is set at
+ * lowerBound + channelNumber * spacing, up to upperBound.
+ */
+struct AmFmBandRange {
+    /** The frequency of the first channel within the range. */
+    uint32_t lowerBound;
+
+    /** The frequency of the last channel within the range. */
+    uint32_t upperBound;
+
+    /** Channel grid resolution, how far apart are the channels. */
+    uint32_t spacing;
+
+    /**
+     * Spacing used when scanning for channels. It's a multiply of spacing and
+     * allows to skip some channels when scanning to make it faster.
+     *
+     * Tuner may first quickly check every n-th channel and if it detects echo
+     * from a station, it fine-tunes to find the exact frequency.
+     *
+     * It's ignored for capabilities check (with full=true when calling
+     * getAmFmRegionConfig).
+     */
+    uint32_t scanSpacing;
+};
+
+/**
+ * An entry in regional configuration for DAB.
+ *
+ * Defines a frequency table row for ensembles.
+ */
+struct DabTableEntry {
+    /**
+     * Channel name, i.e. 5A, 7B.
+     *
+     * It must match the following regular expression: /^[A-Z0-9]{2,5}$/.
+     */
+    string label;
+
+    /** Frequency, in kHz. */
+    uint32_t frequency;
+};
+
+/**
  * Properties of a given broadcast radio module.
  */
 struct Properties {
diff --git a/broadcastradio/2.0/vts/functional/VtsHalBroadcastradioV2_0TargetTest.cpp b/broadcastradio/2.0/vts/functional/VtsHalBroadcastradioV2_0TargetTest.cpp
index cbe6288..87ac934 100644
--- a/broadcastradio/2.0/vts/functional/VtsHalBroadcastradioV2_0TargetTest.cpp
+++ b/broadcastradio/2.0/vts/functional/VtsHalBroadcastradioV2_0TargetTest.cpp
@@ -26,9 +26,11 @@
 #include <broadcastradio-vts-utils/call-barrier.h>
 #include <broadcastradio-vts-utils/mock-timeout.h>
 #include <broadcastradio-vts-utils/pointer-utils.h>
+#include <cutils/bitops.h>
 #include <gmock/gmock.h>
 
 #include <chrono>
+#include <regex>
 
 namespace android {
 namespace hardware {
@@ -93,6 +95,7 @@
     virtual void TearDown() override;
 
     bool openSession();
+    bool getAmFmRegionConfig(bool full, AmFmRegionConfig* config);
 
     sp<IBroadcastRadio> mModule;
     Properties mProperties;
@@ -159,6 +162,22 @@
     return nullptr != mSession.get();
 }
 
+bool BroadcastRadioHalTest::getAmFmRegionConfig(bool full, AmFmRegionConfig* config) {
+    auto halResult = Result::UNKNOWN_ERROR;
+    auto cb = [&](Result result, AmFmRegionConfig configCb) {
+        halResult = result;
+        if (config) *config = configCb;
+    };
+
+    auto hidlResult = mModule->getAmFmRegionConfig(full, cb);
+    EXPECT_TRUE(hidlResult.isOk());
+
+    if (halResult == Result::NOT_SUPPORTED) return false;
+
+    EXPECT_EQ(Result::OK, halResult);
+    return halResult == Result::OK;
+}
+
 /**
  * Test session opening.
  *
@@ -181,6 +200,127 @@
     ASSERT_TRUE(openSession());
 }
 
+static bool isValidAmFmFreq(uint64_t freq) {
+    auto id = utils::make_identifier(IdentifierType::AMFM_FREQUENCY, freq);
+    return utils::isValid(id);
+}
+
+static void validateRange(const AmFmBandRange& range) {
+    EXPECT_TRUE(isValidAmFmFreq(range.lowerBound));
+    EXPECT_TRUE(isValidAmFmFreq(range.upperBound));
+    EXPECT_LT(range.lowerBound, range.upperBound);
+    EXPECT_GT(range.spacing, 0u);
+    EXPECT_EQ(0u, (range.upperBound - range.lowerBound) % range.spacing);
+}
+
+static bool supportsFM(const AmFmRegionConfig& config) {
+    for (auto&& range : config.ranges) {
+        if (utils::getBand(range.lowerBound) == utils::FrequencyBand::FM) return true;
+    }
+    return false;
+}
+
+/**
+ * Test fetching AM/FM regional configuration.
+ *
+ * Verifies that:
+ *  - AM/FM regional configuration is either set at startup or not supported at all by the hardware;
+ *  - there is at least one AM/FM band configured;
+ *  - FM Deemphasis and RDS are correctly configured for FM-capable radio;
+ *  - all channel grids (frequency ranges and spacings) are valid;
+ *  - scan spacing is a multiply of manual spacing value.
+ */
+TEST_F(BroadcastRadioHalTest, GetAmFmRegionConfig) {
+    AmFmRegionConfig config;
+    bool supported = getAmFmRegionConfig(false, &config);
+    if (!supported) {
+        printSkipped("AM/FM not supported");
+        return;
+    }
+
+    EXPECT_GT(config.ranges.size(), 0u);
+    EXPECT_LE(popcountll(config.fmDeemphasis), 1);
+    EXPECT_LE(popcountll(config.fmRds), 1);
+
+    for (auto&& range : config.ranges) {
+        validateRange(range);
+        EXPECT_EQ(0u, range.scanSpacing % range.spacing);
+        EXPECT_GE(range.scanSpacing, range.spacing);
+    }
+
+    if (supportsFM(config)) {
+        EXPECT_EQ(popcountll(config.fmDeemphasis), 1);
+    }
+}
+
+/**
+ * Test fetching AM/FM regional capabilities.
+ *
+ * Verifies that:
+ *  - AM/FM regional capabilities are either available or not supported at all by the hardware;
+ *  - there is at least one AM/FM range supported;
+ *  - there is at least one de-emphasis filter mode supported for FM-capable radio;
+ *  - all channel grids (frequency ranges and spacings) are valid;
+ *  - scan spacing is not set.
+ */
+TEST_F(BroadcastRadioHalTest, GetAmFmRegionConfigCapabilities) {
+    AmFmRegionConfig config;
+    bool supported = getAmFmRegionConfig(true, &config);
+    if (!supported) {
+        printSkipped("AM/FM not supported");
+        return;
+    }
+
+    EXPECT_GT(config.ranges.size(), 0u);
+
+    for (auto&& range : config.ranges) {
+        validateRange(range);
+        EXPECT_EQ(0u, range.scanSpacing);
+    }
+
+    if (supportsFM(config)) {
+        EXPECT_GE(popcountll(config.fmDeemphasis), 1);
+    }
+}
+
+/**
+ * Test fetching DAB regional configuration.
+ *
+ * Verifies that:
+ *  - DAB regional configuration is either set at startup or not supported at all by the hardware;
+ *  - all channel labels match correct format;
+ *  - all channel frequencies are in correct range.
+ */
+TEST_F(BroadcastRadioHalTest, GetDabRegionConfig) {
+    Result halResult;
+    hidl_vec<DabTableEntry> config;
+    auto cb = [&](Result result, hidl_vec<DabTableEntry> configCb) {
+        halResult = result;
+        config = configCb;
+    };
+    auto hidlResult = mModule->getDabRegionConfig(cb);
+    ASSERT_TRUE(hidlResult.isOk());
+
+    if (halResult == Result::NOT_SUPPORTED) {
+        printSkipped("DAB not supported");
+        return;
+    }
+    ASSERT_EQ(Result::OK, halResult);
+
+    std::regex re("^[A-Z0-9]{2,5}$");
+    // double-check correctness of the test
+    ASSERT_TRUE(std::regex_match("5A", re));
+    ASSERT_FALSE(std::regex_match("5a", re));
+    ASSERT_FALSE(std::regex_match("123ABC", re));
+
+    for (auto&& entry : config) {
+        EXPECT_TRUE(std::regex_match(std::string(entry.label), re));
+
+        auto id = utils::make_identifier(IdentifierType::DAB_FREQUENCY, entry.frequency);
+        EXPECT_TRUE(utils::isValid(id));
+    }
+}
+
 /**
  * Test tuning with FM selector.
  *
diff --git a/broadcastradio/common/utils2x/Utils.cpp b/broadcastradio/common/utils2x/Utils.cpp
index e0337b4..d825a7a 100644
--- a/broadcastradio/common/utils2x/Utils.cpp
+++ b/broadcastradio/common/utils2x/Utils.cpp
@@ -89,6 +89,18 @@
     return IdentifierIterator(sel) + 1 /* primary id */ + sel.secondaryIds.size();
 }
 
+FrequencyBand getBand(uint64_t freq) {
+    // keep in sync with
+    // frameworks/base/services/core/java/com/android/server/broadcastradio/hal2/Utils.java
+    if (freq < 30) return FrequencyBand::UNKNOWN;
+    if (freq < 500) return FrequencyBand::AM_LW;
+    if (freq < 1705) return FrequencyBand::AM_MW;
+    if (freq < 30000) return FrequencyBand::AM_SW;
+    if (freq < 60000) return FrequencyBand::UNKNOWN;
+    if (freq < 110000) return FrequencyBand::FM;
+    return FrequencyBand::UNKNOWN;
+}
+
 static bool bothHaveId(const ProgramSelector& a, const ProgramSelector& b,
                        const IdentifierType type) {
     return hasId(a, type) && hasId(b, type);
@@ -194,7 +206,7 @@
     return false;
 }
 
-static bool isValid(const ProgramIdentifier& id) {
+bool isValid(const ProgramIdentifier& id) {
     auto val = id.value;
     bool valid = true;
 
@@ -209,8 +221,10 @@
         case IdentifierType::INVALID:
             expect(false, "IdentifierType::INVALID");
             break;
-        case IdentifierType::AMFM_FREQUENCY:
         case IdentifierType::DAB_FREQUENCY:
+            expect(val > 100000u, "f > 100MHz");
+        // fallthrough
+        case IdentifierType::AMFM_FREQUENCY:
         case IdentifierType::DRMO_FREQUENCY:
             expect(val > 100u, "f > 100kHz");
             expect(val < 10000000u, "f < 10GHz");
diff --git a/broadcastradio/common/utils2x/include/broadcastradio-utils-2x/Utils.h b/broadcastradio/common/utils2x/include/broadcastradio-utils-2x/Utils.h
index bac11fd..e3134f7 100644
--- a/broadcastradio/common/utils2x/include/broadcastradio-utils-2x/Utils.h
+++ b/broadcastradio/common/utils2x/include/broadcastradio-utils-2x/Utils.h
@@ -27,6 +27,14 @@
 namespace broadcastradio {
 namespace utils {
 
+enum class FrequencyBand {
+    UNKNOWN,
+    FM,
+    AM_LW,
+    AM_MW,
+    AM_SW,
+};
+
 V2_0::IdentifierType getType(uint32_t typeAsInt);
 V2_0::IdentifierType getType(const V2_0::ProgramIdentifier& id);
 
@@ -64,6 +72,16 @@
 IdentifierIterator end(const V2_0::ProgramSelector& sel);
 
 /**
+ * Guesses band from the frequency value.
+ *
+ * The band bounds are not exact to cover multiple regions.
+ * The function is biased towards success, i.e. it never returns
+ * FrequencyBand::UNKNOWN for correct frequency, but a result for
+ * incorrect one is undefined (it doesn't have to return UNKNOWN).
+ */
+FrequencyBand getBand(uint64_t frequency);
+
+/**
  * Checks, if {@code pointer} tunes to {@channel}.
  *
  * For example, having a channel {AMFM_FREQUENCY = 103.3}:
@@ -105,6 +123,7 @@
  */
 bool isSupported(const V2_0::Properties& prop, const V2_0::ProgramSelector& sel);
 
+bool isValid(const V2_0::ProgramIdentifier& id);
 bool isValid(const V2_0::ProgramSelector& sel);
 
 V2_0::ProgramIdentifier make_identifier(V2_0::IdentifierType type, uint64_t value);