Add Cts for validation of HotwordDetectedResult

Bug: 190688148
Test: atest CtsVoiceInteractionTestCases
Test: atest CtsVoiceInteractionTestCases --instant
Change-Id: Ia5241ab9330e8ec9d93429cfc3c666afca2f8f9e
diff --git a/tests/tests/voiceinteraction/Android.bp b/tests/tests/voiceinteraction/Android.bp
index df37450..9ba5e42 100644
--- a/tests/tests/voiceinteraction/Android.bp
+++ b/tests/tests/voiceinteraction/Android.bp
@@ -24,6 +24,7 @@
         "ctstestrunner-axt",
         "compatibility-device-util-axt",
         "androidx.test.ext.junit",
+        "testng",
     ],
     srcs: [
         "src/**/*.java",
diff --git a/tests/tests/voiceinteraction/common/src/android/voiceinteraction/common/Utils.java b/tests/tests/voiceinteraction/common/src/android/voiceinteraction/common/Utils.java
index 2aaf0cc..7ec2d60 100644
--- a/tests/tests/voiceinteraction/common/src/android/voiceinteraction/common/Utils.java
+++ b/tests/tests/voiceinteraction/common/src/android/voiceinteraction/common/Utils.java
@@ -18,6 +18,8 @@
 import android.app.VoiceInteractor.PickOptionRequest.Option;
 import android.content.LocusId;
 import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
 import android.util.Log;
 
 import java.util.ArrayList;
@@ -44,6 +46,9 @@
 
     public static final long OPERATION_TIMEOUT_MS = 5000;
 
+    /** CDD restricts the max size of each successful hotword result is 100 bytes. */
+    public static final int MAX_HOTWORD_DETECTED_RESULT_SIZE = 100;
+
     /** Decide which VoiceInteractionService should be started for testing. */
     public static final int HOTWORD_DETECTION_SERVICE_NONE = 0;
     public static final int HOTWORD_DETECTION_SERVICE_BASIC = 1;
@@ -227,4 +232,22 @@
         }
         return false;
     }
+
+    public static int getParcelableSize(Parcelable parcelable) {
+        final Parcel p = Parcel.obtain();
+        parcelable.writeToParcel(p, 0);
+        p.setDataPosition(0);
+        final int size = p.dataSize();
+        p.recycle();
+        return size;
+    }
+
+    public static int bitCount(int value) {
+        int bits = 0;
+        while (value > 0) {
+            bits++;
+            value = value >> 1;
+        }
+        return bits;
+    }
 }
diff --git a/tests/tests/voiceinteraction/service/src/android/voiceinteraction/service/MainHotwordDetectionService.java b/tests/tests/voiceinteraction/service/src/android/voiceinteraction/service/MainHotwordDetectionService.java
index da8c6e8..b23d0ae 100644
--- a/tests/tests/voiceinteraction/service/src/android/voiceinteraction/service/MainHotwordDetectionService.java
+++ b/tests/tests/voiceinteraction/service/src/android/voiceinteraction/service/MainHotwordDetectionService.java
@@ -58,7 +58,27 @@
         // TODO: Check the capture session (needs to be reflectively accessed).
         byte[] data = eventPayload.getTriggerAudio();
         if (data != null && data.length > 0) {
-            callback.onDetected(DETECTED_RESULT);
+            // Create the unaccepted HotwordDetectedResult first to test the protection in the
+            // onDetected callback function of HotwordDetectionService. When the bundle data of
+            // HotwordDetectedResult is larger than max bundle size, it will throw the
+            // IllegalArgumentException.
+            PersistableBundle persistableBundle = new PersistableBundle();
+            HotwordDetectedResult hotwordDetectedResult =
+                    new HotwordDetectedResult.Builder()
+                            .setExtras(persistableBundle)
+                            .build();
+            int key = 0;
+            do {
+                persistableBundle.putInt(Integer.toString(key), 0);
+                key++;
+            } while (Utils.getParcelableSize(persistableBundle)
+                    <= HotwordDetectedResult.getMaxBundleSize());
+
+            try {
+                callback.onDetected(hotwordDetectedResult);
+            } catch (IllegalArgumentException e) {
+                callback.onDetected(DETECTED_RESULT);
+            }
         } else {
             callback.onRejected(REJECTED_RESULT);
         }
diff --git a/tests/tests/voiceinteraction/src/android/voiceinteraction/cts/HotwordDetectedResultTest.java b/tests/tests/voiceinteraction/src/android/voiceinteraction/cts/HotwordDetectedResultTest.java
index 3e245de..81254fdb 100644
--- a/tests/tests/voiceinteraction/src/android/voiceinteraction/cts/HotwordDetectedResultTest.java
+++ b/tests/tests/voiceinteraction/src/android/voiceinteraction/cts/HotwordDetectedResultTest.java
@@ -18,10 +18,14 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.testng.Assert.assertThrows;
+
+import android.media.AudioRecord;
 import android.media.MediaSyncEvent;
 import android.os.Parcel;
 import android.os.PersistableBundle;
 import android.service.voice.HotwordDetectedResult;
+import android.voiceinteraction.common.Utils;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 
@@ -33,17 +37,93 @@
 
     @Test
     public void testHotwordDetectedResult_getMaxBundleSize() throws Exception {
-        assertThat(HotwordDetectedResult.getMaxBundleSize()).isEqualTo(50);
+        assertThat(HotwordDetectedResult.getMaxBundleSize() >= 0).isTrue();
+    }
+
+    @Test
+    public void testHotwordDetectedResult_totalSize() throws Exception {
+        final int bitsForConfidenceLevel = Utils.bitCount(
+                HotwordDetectedResult.CONFIDENCE_LEVEL_VERY_HIGH);
+        final int bitsForHotwordOffsetMillis = Integer.SIZE;
+        final int bitsForHotwordDurationMillis = Integer.SIZE;
+        final int bitsForAudioChannel = Integer.SIZE;
+        final int bitsForHotwordDetectionPersonalized = 1;
+        final int bitsForScore = Utils.bitCount(HotwordDetectedResult.getMaxScore());
+        final int bitsForPersonalizedScore = Utils.bitCount(HotwordDetectedResult.getMaxScore());
+        final int bitsForHotwordPhraseId = Utils.bitCount(
+                HotwordDetectedResult.getMaxHotwordPhraseId());
+
+        final int totalSize =
+                bitsForConfidenceLevel + bitsForHotwordOffsetMillis + bitsForHotwordDurationMillis
+                        + bitsForAudioChannel + bitsForHotwordDetectionPersonalized + bitsForScore
+                        + bitsForPersonalizedScore + bitsForHotwordPhraseId
+                        + HotwordDetectedResult.getMaxBundleSize() * Byte.SIZE;
+
+        assertThat(totalSize <= Utils.MAX_HOTWORD_DETECTED_RESULT_SIZE * Byte.SIZE).isTrue();
+    }
+
+    @Test
+    public void testHotwordDetectedResult_bundleExceedMaxBundleSize() throws Exception {
+        final PersistableBundle persistableBundle = new PersistableBundle();
+        int key = 0;
+        do {
+            persistableBundle.putInt(Integer.toString(key), 0);
+            key++;
+        } while (Utils.getParcelableSize(persistableBundle)
+                <= HotwordDetectedResult.getMaxBundleSize());
+
+        assertThrows(IllegalArgumentException.class,
+                () -> new HotwordDetectedResult.Builder().setExtras(persistableBundle).build());
     }
 
     @Test
     public void testHotwordDetectedResult_getMaxHotwordPhraseId() throws Exception {
-        assertThat(HotwordDetectedResult.getMaxHotwordPhraseId()).isEqualTo(63);
+        assertThat(HotwordDetectedResult.getMaxHotwordPhraseId() >= 63).isTrue();
+    }
+
+    @Test
+    public void testHotwordDetectedResult_setInvalidHotwordPhraseId() throws Exception {
+        assertThrows(IllegalArgumentException.class,
+                () -> new HotwordDetectedResult.Builder().setHotwordPhraseId(-1).build());
+
+        assertThrows(IllegalArgumentException.class,
+                () -> new HotwordDetectedResult.Builder().setHotwordPhraseId(
+                        HotwordDetectedResult.getMaxHotwordPhraseId() + 1).build());
     }
 
     @Test
     public void testHotwordDetectedResult_getMaxScore() throws Exception {
-        assertThat(HotwordDetectedResult.getMaxScore()).isEqualTo(255);
+        assertThat(HotwordDetectedResult.getMaxScore() >= 255).isTrue();
+    }
+
+    @Test
+    public void testHotwordDetectedResult_setInvalidScore() throws Exception {
+        assertThrows(IllegalArgumentException.class,
+                () -> new HotwordDetectedResult.Builder().setScore(-1).build());
+
+        assertThrows(IllegalArgumentException.class,
+                () -> new HotwordDetectedResult.Builder().setScore(
+                        HotwordDetectedResult.getMaxScore() + 1).build());
+    }
+
+    @Test
+    public void testHotwordDetectedResult_setInvalidPersonalizedScore() throws Exception {
+        assertThrows(IllegalArgumentException.class,
+                () -> new HotwordDetectedResult.Builder().setPersonalizedScore(-1).build());
+
+        assertThrows(IllegalArgumentException.class,
+                () -> new HotwordDetectedResult.Builder().setPersonalizedScore(
+                        HotwordDetectedResult.getMaxScore() + 1).build());
+    }
+
+    @Test
+    public void testHotwordDetectedResult_setInvalidHotwordDurationMillis() throws Exception {
+        assertThrows(IllegalArgumentException.class,
+                () -> new HotwordDetectedResult.Builder().setHotwordDurationMillis(-1).build());
+
+        assertThrows(IllegalArgumentException.class,
+                () -> new HotwordDetectedResult.Builder().setHotwordDurationMillis(
+                        (int) AudioRecord.getMaxSharedAudioHistoryMillis() + 1).build());
     }
 
     @Test