Add CTS test for VD-specific session id support in TTS.

Test: atest android.virtualdevice.cts.TextToSpeechTest
Bug: 275634941
Change-Id: I754e8112c099a25642232cea2b2329830b638644
diff --git a/tests/tests/virtualdevice/src/android/virtualdevice/cts/TextToSpeechTest.java b/tests/tests/virtualdevice/src/android/virtualdevice/cts/TextToSpeechTest.java
new file mode 100644
index 0000000..04f8d96
--- /dev/null
+++ b/tests/tests/virtualdevice/src/android/virtualdevice/cts/TextToSpeechTest.java
@@ -0,0 +1,243 @@
+/*
+ * 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.virtualdevice.cts;
+
+import static android.Manifest.permission.ACTIVITY_EMBEDDING;
+import static android.Manifest.permission.ADD_TRUSTED_DISPLAY;
+import static android.Manifest.permission.CREATE_VIRTUAL_DEVICE;
+import static android.Manifest.permission.MODIFY_AUDIO_ROUTING;
+import static android.Manifest.permission.READ_CLIPBOARD_IN_BACKGROUND;
+import static android.Manifest.permission.WAKE_LOCK;
+import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_CUSTOM;
+import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_AUDIO;
+import static android.media.AudioAttributes.CONTENT_TYPE_SPEECH;
+
+import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assume.assumeNotNull;
+import static org.junit.Assume.assumeTrue;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.companion.virtual.VirtualDeviceManager;
+import android.companion.virtual.VirtualDeviceParams;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.media.AudioManager;
+import android.media.AudioPlaybackConfiguration;
+import android.os.Bundle;
+import android.platform.test.annotations.AppModeFull;
+import android.speech.tts.TextToSpeech;
+import android.util.Log;
+import android.virtualdevice.cts.common.FakeAssociationRule;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.compatibility.common.util.AdoptShellPermissionsRule;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.SettableFuture;
+import com.google.common.util.concurrent.Uninterruptibles;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+import java.util.Locale;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+@RunWith(AndroidJUnit4.class)
+@AppModeFull(reason = "VirtualDeviceManager cannot be accessed by instant apps")
+public class TextToSpeechTest {
+    private static final String TAG = TextToSpeechTest.class.getSimpleName();
+    private static final String TTS_TEXT = "My hovercraft is full of eels";
+    private static final String UTTERANCE_ID = "vdmTtsTestUtteranceId";
+
+    @Rule
+    public AdoptShellPermissionsRule mAdoptShellPermissionsRule = new AdoptShellPermissionsRule(
+            InstrumentationRegistry.getInstrumentation().getUiAutomation(),
+            ACTIVITY_EMBEDDING,
+            ADD_TRUSTED_DISPLAY,
+            CREATE_VIRTUAL_DEVICE,
+            READ_CLIPBOARD_IN_BACKGROUND,
+            // Modify audio routing permission is needed because without it, the audio session id
+            // entry in AudioPlaybackConfiguration is redacted.
+            MODIFY_AUDIO_ROUTING,
+            WAKE_LOCK);
+
+    @Rule
+    public FakeAssociationRule mFakeAssociationRule = new FakeAssociationRule();
+
+    private VirtualDeviceManager mVirtualDeviceManager;
+    private AudioManager mAudioManager;
+
+    private SpeechPlaybackObserver mSpeechPlaybackObserver;
+
+    @Before
+    public void setUp() {
+        Context context = getApplicationContext();
+        final PackageManager packageManager = context.getPackageManager();
+        assumeTrue(packageManager.hasSystemFeature(PackageManager.FEATURE_COMPANION_DEVICE_SETUP));
+        assumeTrue(packageManager.hasSystemFeature(
+                PackageManager.FEATURE_ACTIVITIES_ON_SECONDARY_DISPLAYS));
+        mVirtualDeviceManager = context.getSystemService(VirtualDeviceManager.class);
+        assumeNotNull(mVirtualDeviceManager);
+        mAudioManager = context.getSystemService(AudioManager.class);
+        assumeNotNull(mAudioManager);
+        mSpeechPlaybackObserver = new SpeechPlaybackObserver();
+        mAudioManager.registerAudioPlaybackCallback(mSpeechPlaybackObserver, /*handler=*/null);
+
+    }
+
+    @After
+    public void tearDown() {
+        mAudioManager.unregisterAudioPlaybackCallback(mSpeechPlaybackObserver);
+    }
+
+    @Test
+    public void textToSpeechWithVirtualDeviceContext_hasVdmSpecificSessionId() throws Exception {
+        // Create virtual device with device specific audio session id.
+        int virtualDeviceAudioSessionId = mAudioManager.generateAudioSessionId();
+        try (VirtualDeviceManager.VirtualDevice virtualDevice =
+                     createVirtualDeviceWithPlaybackSessionId(
+                             virtualDeviceAudioSessionId)) {
+            Context virtualDeviceContext = virtualDevice.createContext();
+
+            // Instantiate TTS with device-specific context.
+            TextToSpeech tts = initializeTextToSpeech(virtualDeviceContext);
+            assumeNotNull(tts);
+
+            try {
+                tts.speak(TTS_TEXT, TextToSpeech.QUEUE_ADD, /*params=*/null, UTTERANCE_ID);
+
+                // Wait for audio playback with SPEECH content.
+                AudioPlaybackConfiguration ttsAudioPlaybackConfig =
+                        mSpeechPlaybackObserver.getSpeechAudioPlaybackConfigFuture().get();
+
+                // Verify the SPEECH playback has audio session id corresponding to virtual device.
+                assertThat(ttsAudioPlaybackConfig.getSessionId()).isEqualTo(
+                        virtualDeviceAudioSessionId);
+            } finally {
+                tts.shutdown();
+            }
+        }
+    }
+
+    @Test
+    public void textToSpeechWithVirtualDeviceContext_explicitSessionIdOverridesVdmSessionId()
+            throws Exception {
+        // Create virtual device with device specific audio session id.
+        int virtualDeviceAudioSessionId = mAudioManager.generateAudioSessionId();
+        try (VirtualDeviceManager.VirtualDevice virtualDevice =
+                     createVirtualDeviceWithPlaybackSessionId(
+                             virtualDeviceAudioSessionId)) {
+            Context virtualDeviceContext = virtualDevice.createContext();
+
+            // Instantiate TTS with device-specific context.
+            TextToSpeech tts = initializeTextToSpeech(virtualDeviceContext);
+            assumeNotNull(tts);
+
+            try {
+                // Issue TTS.speak request with explicitly configured audio session id.
+                int explicitlyRequestedAudioSessionId = mAudioManager.generateAudioSessionId();
+                tts.speak(TTS_TEXT, TextToSpeech.QUEUE_ADD,
+                        createAudioSessionIdParamForTts(explicitlyRequestedAudioSessionId),
+                        UTTERANCE_ID);
+
+                // Wait for audio playback with SPEECH content.
+                AudioPlaybackConfiguration ttsAudioPlaybackConfig =
+                                mSpeechPlaybackObserver.getSpeechAudioPlaybackConfigFuture().get();
+
+                // Verify that explicitly requested audio session id has overridden the virtual
+                // device audio session id.
+                assertThat(ttsAudioPlaybackConfig.getSessionId()).isEqualTo(
+                        explicitlyRequestedAudioSessionId);
+            } finally {
+                tts.shutdown();
+            }
+        }
+    }
+
+    private static Bundle createAudioSessionIdParamForTts(int sessionId) {
+        Bundle bundle = new Bundle();
+        bundle.putInt(TextToSpeech.Engine.KEY_PARAM_SESSION_ID, sessionId);
+        return bundle;
+    }
+
+    private static @Nullable TextToSpeech initializeTextToSpeech(@NonNull Context context) {
+        SettableFuture<Integer> ttsInitFuture = SettableFuture.create();
+        TextToSpeech tts = new TextToSpeech(context, status -> ttsInitFuture.set(status));
+        int status;
+        try {
+            status = Uninterruptibles.getUninterruptibly(ttsInitFuture, 5, TimeUnit.SECONDS);
+        } catch (ExecutionException | TimeoutException exception) {
+            Log.w(TAG, "Failed to initialize TTS", exception);
+            return null;
+        }
+        if (status != TextToSpeech.SUCCESS) {
+            Log.w(TAG, String.format("TextToSpeech initialization failed with %d", status));
+            return null;
+        }
+        tts.setLanguage(Locale.US);
+        return tts;
+    }
+
+    private VirtualDeviceManager.VirtualDevice createVirtualDeviceWithPlaybackSessionId(
+            int audioPlaybackSessionId) {
+        return mVirtualDeviceManager.createVirtualDevice(
+                mFakeAssociationRule.getAssociationInfo().getId(),
+                new VirtualDeviceParams.Builder()
+                        .setDevicePolicy(POLICY_TYPE_AUDIO, DEVICE_POLICY_CUSTOM)
+                        .setAudioPlaybackSessionId(audioPlaybackSessionId)
+                        .build());
+    }
+
+    /**
+     * Helper class implementing AudioPlaybackCallback to detect playback with SPEECH content type.
+     */
+    private static class SpeechPlaybackObserver extends AudioManager.AudioPlaybackCallback {
+
+        private SettableFuture<AudioPlaybackConfiguration> mAudioPlaybackConfigurationFuture =
+                SettableFuture.create();
+
+        @Override
+        public void onPlaybackConfigChanged(List<AudioPlaybackConfiguration> configs) {
+            super.onPlaybackConfigChanged(configs);
+            configs.stream().filter(c -> c.getAudioAttributes().getContentType()
+                    == CONTENT_TYPE_SPEECH).findAny().ifPresent(
+                    mAudioPlaybackConfigurationFuture::set);
+        }
+
+        /**
+         * Get {@ListenableFuture} with observed SPEECH playb
+         *
+         * @return {@code ListenableFuture} instance which will be completed with
+         *    @code AudioPlaybackConfiguration} corresponding to SPEECH playback once detected.
+         */
+        ListenableFuture<AudioPlaybackConfiguration> getSpeechAudioPlaybackConfigFuture() {
+            return mAudioPlaybackConfigurationFuture;
+        }
+    }
+}