Expose radio metadata image query APIs as system APIs

Promote hidden APIs related to radio metadata image query,
getBitmapId and getMetadata, to system API.

Bug: 318362428
Test: atest CtsBroadcastRadioTestCases
Change-Id: I5187cf717467a7f9093ed121df52c89a9414acdf
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index ea008ac..6a1df16 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -6124,6 +6124,7 @@
     method public boolean containsKey(String);
     method public int describeContents();
     method @Deprecated public android.graphics.Bitmap getBitmap(String);
+    method @FlaggedApi("android.hardware.radio.hd_radio_improved") public int getBitmapId(@NonNull String);
     method public android.hardware.radio.RadioMetadata.Clock getClock(String);
     method public int getInt(String);
     method public String getString(String);
@@ -6187,6 +6188,7 @@
     method @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public abstract void close();
     method @Deprecated @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public abstract int getConfiguration(android.hardware.radio.RadioManager.BandConfig[]);
     method @Nullable @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public android.hardware.radio.ProgramList getDynamicProgramList(@Nullable android.hardware.radio.ProgramList.Filter);
+    method @FlaggedApi("android.hardware.radio.hd_radio_improved") @NonNull @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public android.graphics.Bitmap getMetadataImage(int);
     method @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public abstract boolean getMute();
     method @NonNull @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public java.util.Map<java.lang.String,java.lang.String> getParameters(@NonNull java.util.List<java.lang.String>);
     method @Deprecated @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public abstract int getProgramInformation(android.hardware.radio.RadioManager.ProgramInfo[]);
diff --git a/core/java/android/hardware/radio/RadioMetadata.java b/core/java/android/hardware/radio/RadioMetadata.java
index db14c08..da6b9c2 100644
--- a/core/java/android/hardware/radio/RadioMetadata.java
+++ b/core/java/android/hardware/radio/RadioMetadata.java
@@ -507,10 +507,16 @@
      *
      * @param key The key the value is stored under.
      * @return a bitmap identifier or 0 if it's missing.
-     * @hide This API is not thoroughly elaborated yet
+     * @throws NullPointerException if metadata key is {@code null}
+     * @throws IllegalArgumentException if the metadata with the key is not found in
+     * metadata or the key is not of bitmap-key type
      */
+    @FlaggedApi(Flags.FLAG_HD_RADIO_IMPROVED)
     public int getBitmapId(@NonNull String key) {
-        if (!METADATA_KEY_ICON.equals(key) && !METADATA_KEY_ART.equals(key)) return 0;
+        Objects.requireNonNull(key, "Metadata key can not be null");
+        if (!METADATA_KEY_ICON.equals(key) && !METADATA_KEY_ART.equals(key)) {
+            throw new IllegalArgumentException("Failed to retrieve key " + key + " as bitmap key");
+        }
         return getInt(key);
     }
 
diff --git a/core/java/android/hardware/radio/RadioTuner.java b/core/java/android/hardware/radio/RadioTuner.java
index 9b2bcde..7c5c003 100644
--- a/core/java/android/hardware/radio/RadioTuner.java
+++ b/core/java/android/hardware/radio/RadioTuner.java
@@ -17,6 +17,7 @@
 package android.hardware.radio;
 
 import android.Manifest;
+import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -332,29 +333,30 @@
     public abstract int getProgramInformation(RadioManager.ProgramInfo[] info);
 
     /**
-     * Retrieves a {@link Bitmap} for the given image ID or null,
+     * Retrieves a {@link Bitmap} for the given image ID or throw {@link IllegalArgumentException},
      * if the image was missing from the tuner.
      *
      * <p>This involves doing a call to the tuner, so the bitmap should be cached
      * on the application side.
      *
-     * <p>If the method returns null for non-zero ID, it means the image was
-     * updated on the tuner side. There is a race conditon between fetching
-     * image for an old ID and tuner updating the image (and cleaning up the
+     * <p>If the method throws {@link IllegalArgumentException} for non-zero ID, it
+     * means the image was updated on the tuner side. There is a race condition between
+     * fetching image for an old ID and tuner updating the image (and cleaning up the
      * old image). In such case, a new ProgramInfo with updated image id will
      * be sent with a {@link Callback#onProgramInfoChanged(RadioManager.ProgramInfo)}
      * callback.
      *
      * @param id The image identifier, retrieved with
      *           {@link RadioMetadata#getBitmapId(String)}.
-     * @return A {@link Bitmap} or null.
-     * @throws IllegalArgumentException if id==0
-     * @hide This API is not thoroughly elaborated yet
+     * @return A {@link Bitmap} for the given image ID.
+     * @throws IllegalArgumentException if id is 0 or the referenced image id no longer exists.
      */
-    @SuppressWarnings("HiddenAbstractMethod")
+    @FlaggedApi(Flags.FLAG_HD_RADIO_IMPROVED)
     @RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO)
-    public abstract @Nullable Bitmap getMetadataImage(int id);
-
+    public @NonNull Bitmap getMetadataImage(int id) {
+        throw new UnsupportedOperationException(
+                "Getting metadata image must be implemented in child classes");
+    }
     /**
      * Initiates a background scan to update internally cached program list.
      *
diff --git a/core/java/android/hardware/radio/TunerAdapter.java b/core/java/android/hardware/radio/TunerAdapter.java
index ba31ca3..63b2d4c 100644
--- a/core/java/android/hardware/radio/TunerAdapter.java
+++ b/core/java/android/hardware/radio/TunerAdapter.java
@@ -16,6 +16,7 @@
 
 package android.hardware.radio;
 
+import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.graphics.Bitmap;
 import android.os.RemoteException;
@@ -251,10 +252,18 @@
     }
 
     @Override
-    @Nullable
+    @NonNull
     public Bitmap getMetadataImage(int id) {
+        if (id == 0) {
+            throw new IllegalArgumentException("Invalid metadata image id 0");
+        }
         try {
-            return mTuner.getImage(id);
+            Bitmap bitmap = mTuner.getImage(id);
+            if (bitmap == null) {
+                throw new IllegalArgumentException("Metadata image with id " + id
+                        + " is not available");
+            }
+            return bitmap;
         } catch (RemoteException e) {
             throw new RuntimeException("Service died", e);
         }
diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/DefaultRadioTunerTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/DefaultRadioTunerTest.java
index 63de759..ddf3615 100644
--- a/core/tests/BroadcastRadioTests/src/android/hardware/radio/DefaultRadioTunerTest.java
+++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/DefaultRadioTunerTest.java
@@ -20,8 +20,6 @@
 
 import static org.junit.Assert.assertThrows;
 
-import android.graphics.Bitmap;
-
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
@@ -88,12 +86,6 @@
             return 0;
         }
 
-        @Nullable
-        @Override
-        public Bitmap getMetadataImage(int id) {
-            return null;
-        }
-
         @Override
         public boolean startBackgroundScan() {
             return false;
@@ -138,6 +130,16 @@
     }
 
     @Test
+    public void getMetadataImage_forRadioTuner_throwsException() {
+        UnsupportedOperationException thrown = assertThrows(UnsupportedOperationException.class,
+                () -> DEFAULT_RADIO_TUNER.getMetadataImage(/* id= */ 1));
+
+        assertWithMessage("Exception for getting metadata image from default radio tuner")
+                .that(thrown).hasMessageThat()
+                .contains("Getting metadata image must be implemented in child classes");
+    }
+
+    @Test
     public void getDynamicProgramList_forRadioTuner_returnsNull() {
         assertWithMessage("Dynamic program list obtained from default radio tuner")
                 .that(DEFAULT_RADIO_TUNER.getDynamicProgramList(new ProgramList.Filter())).isNull();
diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/RadioMetadataTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/RadioMetadataTest.java
index fddfd39..b3a0aba 100644
--- a/core/tests/BroadcastRadioTests/src/android/hardware/radio/RadioMetadataTest.java
+++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/RadioMetadataTest.java
@@ -290,13 +290,29 @@
     }
 
     @Test
-    public void getBitmapId_withIllegalKey() {
+    public void getBitmapId_withIllegalKey_fails() {
         String illegalKey = RadioMetadata.METADATA_KEY_ARTIST;
         RadioMetadata metadata = mBuilder.putInt(RadioMetadata.METADATA_KEY_ART, INT_KEY_VALUE)
                 .build();
 
-        mExpect.withMessage("Bitmap id value with non-bitmap-id-type key %s", illegalKey)
-                .that(metadata.getBitmapId(illegalKey)).isEqualTo(0);
+        IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, () -> {
+            metadata.getBitmapId(illegalKey);
+        });
+
+        mExpect.withMessage("Exception for getting string array for non-bitmap-id type key %s",
+                illegalKey).that(thrown).hasMessageThat().contains("bitmap key");
+    }
+
+    @Test
+    public void getBitmapId_withNullKey_fails() {
+        RadioMetadata metadata = mBuilder.build();
+
+        NullPointerException thrown = assertThrows(NullPointerException.class, () -> {
+            metadata.getBitmapId(/* key= */ null);
+        });
+
+        mExpect.withMessage("Exception for getting bitmap identifier with null key")
+                .that(thrown).hasMessageThat().contains("can not be null");
     }
 
     @Test
diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/TunerAdapterTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/TunerAdapterTest.java
index 4cda26d..5aace81 100644
--- a/core/tests/BroadcastRadioTests/src/android/hardware/radio/TunerAdapterTest.java
+++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/TunerAdapterTest.java
@@ -514,6 +514,26 @@
     }
 
     @Test
+    public void getMetadataImage_withImageIdUnavailable_fails() throws Exception {
+        int nonExistImageId = 2;
+        when(mTunerMock.getImage(nonExistImageId)).thenReturn(null);
+        IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class,
+                () -> mRadioTuner.getMetadataImage(nonExistImageId));
+
+        assertWithMessage("Exception for getting metadata image with non-existing id")
+                .that(thrown).hasMessageThat().contains("is not available");
+    }
+
+    @Test
+    public void getMetadataImage_withInvalidId_fails() {
+        IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class,
+                () -> mRadioTuner.getMetadataImage(/* id= */ 0));
+
+        assertWithMessage("Exception for getting metadata image for id 0").that(thrown)
+                .hasMessageThat().contains("Invalid metadata image id 0");
+    }
+
+    @Test
     public void getMetadataImage_whenServiceDied_fails() throws Exception {
         when(mTunerMock.getImage(anyInt())).thenThrow(new RemoteException());