blob: 04f8d96aeff1ba176e49233fd013b3b84f58cfcd [file] [log] [blame]
/*
* 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;
}
}
}