Merge "Add unique program identifier for broadcast radio" into main
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);
+    }
+}