Add unique program identifier for broadcast radio

Added unique program identifier in program selector to uniquely specify
DAB (and other technonlogies in the fulture) stations with the same
primary id but different ensemble or frequency values in radio manager
and broadcast radio service.

Bug: 273967346
Test: atest ProgramSelectorTest

Change-Id: I8ba800d0f8e0a2f1a65dd47be47d6564c8c608cd
diff --git a/core/java/android/hardware/radio/UniqueProgramIdentifier.aidl b/core/java/android/hardware/radio/UniqueProgramIdentifier.aidl
new file mode 100644
index 0000000..2ed2bcc
--- /dev/null
+++ b/core/java/android/hardware/radio/UniqueProgramIdentifier.aidl
@@ -0,0 +1,20 @@
+/**
+ * Copyright (C) 2023 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 UniqueProgramIdentifier;
diff --git a/core/java/android/hardware/radio/UniqueProgramIdentifier.java b/core/java/android/hardware/radio/UniqueProgramIdentifier.java
new file mode 100644
index 0000000..ea8948e
--- /dev/null
+++ b/core/java/android/hardware/radio/UniqueProgramIdentifier.java
@@ -0,0 +1,163 @@
+/**
+ * Copyright (C) 2023 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.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Stream;
+
+/**
+ * Identifier that can uniquely identifies a program.
+ *
+ * This is a transport class used for internal communication between
+ * Broadcast Radio Service and Radio Manager. Do not use it directly.
+ *
+ * @hide
+ */
+public final class UniqueProgramIdentifier implements Parcelable {
+
+    @NonNull private final ProgramSelector.Identifier mPrimaryId;
+    @NonNull private final ProgramSelector.Identifier[] mCriticalSecondaryIds;
+
+    /**
+     * Check whether some secondary identifier is needed to uniquely specify a program for
+     * a given primary identifier type
+     *
+     * @param type primary identifier type {@link ProgramSelector.IdentifierType}
+     * @return whether some secondary identifier is needed to uniquely specify a program.
+     */
+    public static boolean requireCriticalSecondaryIds(@ProgramSelector.IdentifierType int type) {
+        return type == ProgramSelector.IDENTIFIER_TYPE_DAB_DMB_SID_EXT || type
+                == ProgramSelector.IDENTIFIER_TYPE_DAB_SID_EXT;
+    }
+
+    public UniqueProgramIdentifier(ProgramSelector selector) {
+        Objects.requireNonNull(selector, "Program selector can not be null");
+        mPrimaryId = selector.getPrimaryId();
+        switch (mPrimaryId.getType()) {
+            case ProgramSelector.IDENTIFIER_TYPE_DAB_DMB_SID_EXT:
+            case ProgramSelector.IDENTIFIER_TYPE_DAB_SID_EXT:
+                ProgramSelector.Identifier ensembleId = null;
+                ProgramSelector.Identifier frequencyId = null;
+                ProgramSelector.Identifier[] secondaryIds = selector.getSecondaryIds();
+                for (int i = 0; i < secondaryIds.length; i++) {
+                    if (ensembleId == null && secondaryIds[i].getType()
+                            == ProgramSelector.IDENTIFIER_TYPE_DAB_ENSEMBLE) {
+                        ensembleId = selector.getSecondaryIds()[i];
+                    } else if (frequencyId == null && secondaryIds[i].getType()
+                            == ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY) {
+                        frequencyId = secondaryIds[i];
+                    }
+                    if (ensembleId != null && frequencyId != null) {
+                        break;
+                    }
+                }
+                if (ensembleId == null) {
+                    if (frequencyId == null) {
+                        mCriticalSecondaryIds = new ProgramSelector.Identifier[]{};
+                    } else {
+                        mCriticalSecondaryIds = new ProgramSelector.Identifier[]{frequencyId};
+                    }
+                } else if (frequencyId == null) {
+                    mCriticalSecondaryIds = new ProgramSelector.Identifier[]{ensembleId};
+                } else {
+                    mCriticalSecondaryIds = new ProgramSelector.Identifier[]{ensembleId,
+                            frequencyId};
+                }
+                break;
+            default:
+                mCriticalSecondaryIds = new ProgramSelector.Identifier[]{};
+        }
+
+    }
+
+    public UniqueProgramIdentifier(ProgramSelector.Identifier primaryId) {
+        mPrimaryId = primaryId;
+        mCriticalSecondaryIds = new ProgramSelector.Identifier[]{};
+    }
+
+    @NonNull
+    public ProgramSelector.Identifier getPrimaryId() {
+        return mPrimaryId;
+    }
+
+    @NonNull
+    public List<ProgramSelector.Identifier> getCriticalSecondaryIds() {
+        return List.of(mCriticalSecondaryIds);
+    }
+
+    @NonNull
+    @Override
+    public String toString() {
+        return new StringBuilder("UniqueProgramIdentifier(primary=").append(mPrimaryId)
+                .append(", criticalSecondary=")
+                .append(Arrays.toString(mCriticalSecondaryIds)).append(")")
+                .toString();
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mPrimaryId, Arrays.hashCode(mCriticalSecondaryIds));
+    }
+
+    @Override
+    public boolean equals(@Nullable Object obj) {
+        if (this == obj) return true;
+        if (!(obj instanceof UniqueProgramIdentifier)) return false;
+        UniqueProgramIdentifier other = (UniqueProgramIdentifier) obj;
+        return other.mPrimaryId.equals(mPrimaryId)
+                && Arrays.equals(other.mCriticalSecondaryIds, mCriticalSecondaryIds);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    private UniqueProgramIdentifier(Parcel in) {
+        mPrimaryId = in.readTypedObject(ProgramSelector.Identifier.CREATOR);
+        mCriticalSecondaryIds = in.createTypedArray(ProgramSelector.Identifier.CREATOR);
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeTypedObject(mPrimaryId, 0);
+        dest.writeTypedArray(mCriticalSecondaryIds, 0);
+        if (Stream.of(mCriticalSecondaryIds).anyMatch(Objects::isNull)) {
+            throw new IllegalArgumentException(
+                    "criticalSecondaryIds list must not contain nulls");
+        }
+    }
+
+    @NonNull
+    public static final Parcelable.Creator<UniqueProgramIdentifier> CREATOR =
+            new Parcelable.Creator<UniqueProgramIdentifier>() {
+                public UniqueProgramIdentifier createFromParcel(Parcel in) {
+                    return new UniqueProgramIdentifier(in);
+                }
+
+                public UniqueProgramIdentifier[] newArray(int size) {
+                    return new UniqueProgramIdentifier[size];
+                }
+            };
+}
diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/ProgramSelectorTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/ProgramSelectorTest.java
index ae43a1c..b1cf9c2 100644
--- a/core/tests/BroadcastRadioTests/src/android/hardware/radio/ProgramSelectorTest.java
+++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/ProgramSelectorTest.java
@@ -437,8 +437,8 @@
 
     @Test
     public void writeToParcel_forProgramSelector() {
-        ProgramSelector selectorExpected =
-                getFmSelector(/* secondaryIds= */ null, /* vendorIds= */ null);
+        ProgramSelector selectorExpected = getDabSelector(new ProgramSelector.Identifier[]{
+                DAB_ENSEMBLE_IDENTIFIER, DAB_FREQUENCY_IDENTIFIER}, /* vendorIds= */ null);
         Parcel parcel = Parcel.obtain();
 
         selectorExpected.writeToParcel(parcel, /* flags= */ 0);
diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/UniqueProgramIdentifierTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/UniqueProgramIdentifierTest.java
new file mode 100644
index 0000000..b36367b
--- /dev/null
+++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/UniqueProgramIdentifierTest.java
@@ -0,0 +1,187 @@
+/**
+ * Copyright (C) 2023 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.Nullable;
+import android.os.Parcel;
+
+import com.google.common.truth.Expect;
+
+import org.junit.Rule;
+import org.junit.Test;
+
+public final class UniqueProgramIdentifierTest {
+    private static final ProgramSelector.Identifier FM_IDENTIFIER = new ProgramSelector.Identifier(
+            ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY, /* value= */ 88_500);
+
+    private static final ProgramSelector.Identifier DAB_DMB_SID_EXT_IDENTIFIER_1 =
+            new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_DMB_SID_EXT,
+                    /* value= */ 0xA000000111L);
+    private static final ProgramSelector.Identifier DAB_ENSEMBLE_IDENTIFIER =
+            new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_ENSEMBLE,
+                    /* value= */ 0x1001);
+    private static final ProgramSelector.Identifier DAB_FREQUENCY_IDENTIFIER =
+            new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY,
+                    /* value= */ 220352);
+    private static final ProgramSelector.Identifier DAB_SCID_IDENTIFIER =
+            new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_SCID,
+                    /* value= */ 0x101);
+
+    @Rule
+    public final Expect expect = Expect.create();
+
+    @Test
+    public void getPrimaryId_forUniqueProgramIdentifier() {
+        ProgramSelector dabSelector = getDabSelector(new ProgramSelector.Identifier[]{
+                DAB_ENSEMBLE_IDENTIFIER, DAB_FREQUENCY_IDENTIFIER}, /* vendorIds= */ null);
+        UniqueProgramIdentifier dabIdentifier = new UniqueProgramIdentifier(dabSelector);
+
+        expect.withMessage("Primary id of DAB unique identifier")
+                .that(dabIdentifier.getPrimaryId()).isEqualTo(DAB_DMB_SID_EXT_IDENTIFIER_1);
+    }
+
+    @Test
+    public void getCriticalSecondaryIds_forDabUniqueProgramIdentifier() {
+        ProgramSelector dabSelector = getDabSelector(new ProgramSelector.Identifier[]{
+                DAB_ENSEMBLE_IDENTIFIER, DAB_FREQUENCY_IDENTIFIER, DAB_SCID_IDENTIFIER},
+                /* vendorIds= */ null);
+        UniqueProgramIdentifier dabIdentifier = new UniqueProgramIdentifier(dabSelector);
+
+        expect.withMessage("Critical secondary ids of DAB unique identifier")
+                .that(dabIdentifier.getCriticalSecondaryIds()).containsExactly(
+                        DAB_ENSEMBLE_IDENTIFIER, DAB_FREQUENCY_IDENTIFIER);
+    }
+
+    @Test
+    public void getCriticalSecondaryIds_forFmUniqueProgramIdentifier() {
+        UniqueProgramIdentifier fmUniqueIdentifier = new UniqueProgramIdentifier(
+                new ProgramSelector(ProgramSelector.PROGRAM_TYPE_FM, FM_IDENTIFIER,
+                        new ProgramSelector.Identifier[]{new ProgramSelector.Identifier(
+                                ProgramSelector.IDENTIFIER_TYPE_RDS_PI, /* value= */ 0x1003)},
+                        /* vendorIds= */ null));
+
+        expect.withMessage("Empty critical secondary id list of FM unique identifier")
+                .that(fmUniqueIdentifier.getCriticalSecondaryIds()).isEmpty();
+    }
+
+    @Test
+    public void toString_forUniqueProgramIdentifier() {
+        ProgramSelector dabSelector = getDabSelector(new ProgramSelector.Identifier[]{
+                DAB_ENSEMBLE_IDENTIFIER, DAB_FREQUENCY_IDENTIFIER}, /* vendorIds= */ null);
+        UniqueProgramIdentifier dabIdentifier = new UniqueProgramIdentifier(dabSelector);
+
+        String identifierString = dabIdentifier.toString();
+
+        expect.withMessage("Primary id in DAB unique identifier")
+                .that(identifierString).contains(DAB_DMB_SID_EXT_IDENTIFIER_1.toString());
+        expect.withMessage("Ensemble id in DAB unique identifier")
+                .that(identifierString).contains(DAB_ENSEMBLE_IDENTIFIER.toString());
+        expect.withMessage("Frequency id in DAB unique identifier")
+                .that(identifierString).contains(DAB_FREQUENCY_IDENTIFIER.toString());
+    }
+
+    @Test
+    public void hashCode_withTheSameUniqueProgramIdentifier_equals() {
+        ProgramSelector dabSelector1 = getDabSelector(new ProgramSelector.Identifier[]{
+                DAB_ENSEMBLE_IDENTIFIER, DAB_FREQUENCY_IDENTIFIER}, /* vendorIds= */ null);
+        ProgramSelector dabSelector2 = getDabSelector(new ProgramSelector.Identifier[]{
+                DAB_FREQUENCY_IDENTIFIER, DAB_ENSEMBLE_IDENTIFIER}, /* vendorIds= */ null);
+        UniqueProgramIdentifier dabIdentifier1 = new UniqueProgramIdentifier(dabSelector1);
+        UniqueProgramIdentifier dabIdentifier2 = new UniqueProgramIdentifier(dabSelector2);
+
+        expect.withMessage("Hash code of the same DAB unique identifiers")
+                .that(dabIdentifier1.hashCode()).isEqualTo(dabIdentifier2.hashCode());
+    }
+
+    @Test
+    public void equals_withIdsForUniqueProgramIdentifier_returnsTrue() {
+        ProgramSelector dabSelector1 = getDabSelector(new ProgramSelector.Identifier[]{
+                DAB_ENSEMBLE_IDENTIFIER, DAB_FREQUENCY_IDENTIFIER}, /* vendorIds= */ null);
+        ProgramSelector dabSelector2 = getDabSelector(new ProgramSelector.Identifier[]{
+                DAB_FREQUENCY_IDENTIFIER, DAB_ENSEMBLE_IDENTIFIER}, /* vendorIds= */ null);
+        UniqueProgramIdentifier dabIdentifier1 = new UniqueProgramIdentifier(dabSelector1);
+        UniqueProgramIdentifier dabIdentifier2 = new UniqueProgramIdentifier(dabSelector2);
+
+        expect.withMessage("The same DAB unique identifiers")
+                .that(dabIdentifier1).isEqualTo(dabIdentifier2);
+    }
+
+    @Test
+    public void equals_withDifferentPrimaryIdsForUniqueProgramIdentifier_returnsFalse() {
+        ProgramSelector dabSelector1 = getDabSelector(new ProgramSelector.Identifier[]{
+                DAB_ENSEMBLE_IDENTIFIER, DAB_FREQUENCY_IDENTIFIER}, /* vendorIds= */ null);
+        UniqueProgramIdentifier dabIdentifier1 = new UniqueProgramIdentifier(dabSelector1);
+        UniqueProgramIdentifier fmUniqueIdentifier = new UniqueProgramIdentifier(FM_IDENTIFIER);
+
+        expect.withMessage("Unique identifier with different primary ids")
+                .that(dabIdentifier1).isNotEqualTo(fmUniqueIdentifier);
+    }
+
+    @Test
+    public void equals_withDifferentSecondaryIdsForUniqueProgramIdentifier_returnsFalse() {
+        ProgramSelector dabSelector1 = getDabSelector(new ProgramSelector.Identifier[]{
+                DAB_ENSEMBLE_IDENTIFIER, DAB_FREQUENCY_IDENTIFIER}, /* vendorIds= */ null);
+        ProgramSelector.Identifier dabFreqIdentifier2 = new ProgramSelector.Identifier(
+                ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY, /* value= */ 222064);
+        ProgramSelector dabSelector2 = getDabSelector(new ProgramSelector.Identifier[]{
+                DAB_ENSEMBLE_IDENTIFIER, dabFreqIdentifier2}, /* vendorIds= */ null);
+        UniqueProgramIdentifier dabIdentifier1 = new UniqueProgramIdentifier(dabSelector1);
+        UniqueProgramIdentifier dabIdentifier2 = new UniqueProgramIdentifier(dabSelector2);
+
+        expect.withMessage("DAB unique identifier with different secondary ids")
+                .that(dabIdentifier1).isNotEqualTo(dabIdentifier2);
+    }
+
+    @Test
+    public void describeContents_forUniqueProgramIdentifier() {
+        UniqueProgramIdentifier fmUniqueIdentifier = new UniqueProgramIdentifier(FM_IDENTIFIER);
+
+        expect.withMessage("FM unique identifier contents")
+                .that(fmUniqueIdentifier.describeContents()).isEqualTo(0);
+    }
+
+    @Test
+    public void newArray_forUniqueProgramIdentifier() {
+        int createArraySize = 3;
+        UniqueProgramIdentifier[] identifiers = UniqueProgramIdentifier.CREATOR.newArray(
+                createArraySize);
+
+        expect.withMessage("Unique identifiers").that(identifiers).hasLength(createArraySize);
+    }
+
+    @Test
+    public void writeToParcel_forUniqueProgramIdentifier() {
+        ProgramSelector dabSelector = getDabSelector(new ProgramSelector.Identifier[]{
+                DAB_ENSEMBLE_IDENTIFIER, DAB_FREQUENCY_IDENTIFIER}, /* vendorIds= */ null);
+        UniqueProgramIdentifier dabIdentifier = new UniqueProgramIdentifier(dabSelector);
+        Parcel parcel = Parcel.obtain();
+
+        dabIdentifier.writeToParcel(parcel, /* flags= */ 0);
+        parcel.setDataPosition(0);
+
+        UniqueProgramIdentifier identifierFromParcel = UniqueProgramIdentifier.CREATOR
+                .createFromParcel(parcel);
+        expect.withMessage("Unique identifier created from parcel")
+                .that(identifierFromParcel).isEqualTo(dabIdentifier);
+    }
+
+    private ProgramSelector getDabSelector(@Nullable ProgramSelector.Identifier[] secondaryIds,
+            @Nullable long[] vendorIds) {
+        return new ProgramSelector(ProgramSelector.PROGRAM_TYPE_DAB, DAB_DMB_SID_EXT_IDENTIFIER_1,
+                secondaryIds, vendorIds);
+    }
+}