| /* |
| * Copyright (C) 2016 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.media.audio.cts; |
| |
| import static android.app.AppOpsManager.MODE_ALLOWED; |
| import static android.app.AppOpsManager.MODE_IGNORED; |
| import static android.app.AppOpsManager.OPSTR_PLAY_AUDIO; |
| import static android.media.AudioAttributes.ALLOW_CAPTURE_BY_ALL; |
| import static android.media.AudioAttributes.ALLOW_CAPTURE_BY_NONE; |
| import static android.media.AudioAttributes.ALLOW_CAPTURE_BY_SYSTEM; |
| import static android.media.AudioManager.ADJUST_MUTE; |
| import static android.media.AudioManager.ADJUST_UNMUTE; |
| import static android.media.AudioManager.STREAM_NOTIFICATION; |
| import static android.media.AudioPlaybackConfiguration.MUTED_BY_APP_OPS; |
| import static android.media.AudioPlaybackConfiguration.MUTED_BY_CLIENT_VOLUME; |
| import static android.media.AudioPlaybackConfiguration.MUTED_BY_STREAM_VOLUME; |
| import static android.media.AudioPlaybackConfiguration.MUTED_BY_VOLUME_SHAPER; |
| import static android.media.AudioTrack.WRITE_NON_BLOCKING; |
| import static android.media.cts.AudioHelper.createSoundDataInShortByteBuffer; |
| |
| import static com.android.compatibility.common.util.AppOpsUtils.getOpMode; |
| import static com.android.compatibility.common.util.AppOpsUtils.setOpMode; |
| |
| import android.Manifest; |
| import android.annotation.Nullable; |
| import android.annotation.RawRes; |
| import android.content.Context; |
| import android.content.pm.PackageManager; |
| import android.content.res.AssetFileDescriptor; |
| import android.media.AudioAttributes; |
| import android.media.AudioAttributes.CapturePolicy; |
| import android.media.AudioFormat; |
| import android.media.AudioManager; |
| import android.media.AudioPlaybackConfiguration; |
| import android.media.AudioTrack; |
| import android.media.MediaPlayer; |
| import android.media.SoundPool; |
| import android.media.VolumeShaper; |
| import android.media.cts.NonMediaMainlineTest; |
| import android.media.cts.TestUtils; |
| import android.os.Handler; |
| import android.os.HandlerThread; |
| import android.os.Parcel; |
| import android.util.Log; |
| |
| import androidx.test.platform.app.InstrumentationRegistry; |
| |
| import com.android.compatibility.common.util.ApiTest; |
| import com.android.compatibility.common.util.CtsAndroidTestCase; |
| import com.android.internal.annotations.GuardedBy; |
| |
| import java.io.IOException; |
| import java.lang.reflect.Method; |
| import java.nio.ByteBuffer; |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.function.Consumer; |
| import java.util.function.Function; |
| |
| @NonMediaMainlineTest |
| public class AudioPlaybackConfigurationTest extends CtsAndroidTestCase { |
| private final static String TAG = "AudioPlaybackConfigurationTest"; |
| |
| private final static int TEST_TIMING_TOLERANCE_MS = 150; |
| /** acceptable timeout for the time it takes for a prepared MediaPlayer to have an audio device |
| * selected and reported when starting to play */ |
| private final static int PLAY_ROUTING_TIMING_TOLERANCE_MS = 500; |
| private final static int TEST_TIMEOUT_SOUNDPOOL_LOAD_MS = 3000; |
| private final static long MEDIAPLAYER_PREPARE_TIMEOUT_MS = 2000; |
| |
| private static final int TEST_AUDIO_TRACK_SAMPLERATE = 48000; |
| private static final double TEST_AUDIO_TRACK_FREQUENCY = 440.0; |
| private static final int TEST_AUDIO_TRACK_CHANNELS = 2; |
| private static final int TEST_AUDIO_TRACK_PLAY_SECONDS = 2; |
| private static final double TEST_AUDIO_TRACK_SWEEP = 0; |
| |
| // volume shaper duration in milliseconds. |
| private static final long VOLUME_SHAPER_DURATION_MS = 10; |
| |
| private static final VolumeShaper.Configuration SHAPER_MUTE = |
| new VolumeShaper.Configuration.Builder() |
| .setInterpolatorType(VolumeShaper.Configuration.INTERPOLATOR_TYPE_LINEAR) |
| .setCurve(new float[] { 0.f, 1.f } /* times */, |
| new float[] { 1.f, 0.f } /* volumes */) |
| .setDuration(VOLUME_SHAPER_DURATION_MS) |
| .build(); |
| |
| private VolumeShaper mMuteShaper; |
| |
| // not declared inside test so it can be released in case of failure |
| private MediaPlayer mMp; |
| private SoundPool mSp; |
| private AudioTrack mAt; |
| |
| @Override |
| protected void setUp() throws Exception { |
| super.setUp(); |
| } |
| |
| @Override |
| protected void tearDown() throws Exception { |
| super.tearDown(); |
| // try/catch for every method in case the tests left the objects in various states |
| if (mMp != null) { |
| try { |
| mMp.stop(); |
| } catch (Exception ignored) { } |
| mMp.release(); |
| mMp = null; |
| } |
| if (mSp != null) { |
| mSp.release(); |
| mSp = null; |
| } |
| if (mAt != null) { |
| mAt.release(); |
| mAt = null; |
| } |
| } |
| |
| private final static int TEST_USAGE = AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_DELAYED; |
| private final static int TEST_CONTENT = AudioAttributes.CONTENT_TYPE_SPEECH; |
| |
| // test marshalling/unmarshalling of an AudioPlaybackConfiguration instance. Since we can't |
| // create an AudioPlaybackConfiguration directly, we first need to play something to get one. |
| public void testParcelableWriteToParcel() throws Exception { |
| if (!isValidPlatform("testParcelableWriteToParcel")) return; |
| if (hasAudioSilentProperty()) { |
| // No reasons to test since the started MediaPlayer will be muted and inactive |
| Log.w(TAG, "Skipping testParcelableWriteToParcel"); |
| return; |
| } |
| |
| // create a player, make it play so we can get an AudioPlaybackConfiguration instance |
| AudioManager am = new AudioManager(getContext()); |
| assertNotNull("Could not create AudioManager", am); |
| final AudioAttributes aa = (new AudioAttributes.Builder()) |
| .setUsage(TEST_USAGE) |
| .setContentType(TEST_CONTENT) |
| .setAllowedCapturePolicy(ALLOW_CAPTURE_BY_NONE) |
| .build(); |
| mMp = createPreparedMediaPlayer(R.raw.sine1khzs40dblong, aa, am.generateAudioSessionId()); |
| mMp.start(); |
| Thread.sleep(TEST_TIMING_TOLERANCE_MS);// waiting for playback to start |
| List<AudioPlaybackConfiguration> configs = am.getActivePlaybackConfigurations(); |
| mMp.stop(); |
| assertTrue("No playback reported", configs.size() > 0); |
| AudioPlaybackConfiguration configToMarshall = null; |
| for (AudioPlaybackConfiguration config : configs) { |
| if (config.getAudioAttributes().equals(aa)) { |
| configToMarshall = config; |
| break; |
| } |
| } |
| |
| assertNotNull("Configuration not found during playback", configToMarshall); |
| assertEquals(0, configToMarshall.describeContents()); |
| |
| final Parcel srcParcel = Parcel.obtain(); |
| final Parcel dstParcel = Parcel.obtain(); |
| final byte[] mbytes; |
| |
| configToMarshall.writeToParcel(srcParcel, 0 /*no public flags for marshalling*/); |
| mbytes = srcParcel.marshall(); |
| dstParcel.unmarshall(mbytes, 0, mbytes.length); |
| dstParcel.setDataPosition(0); |
| final AudioPlaybackConfiguration restoredConfig = |
| AudioPlaybackConfiguration.CREATOR.createFromParcel(dstParcel); |
| |
| assertEquals("Marshalled/restored AudioAttributes don't match", |
| configToMarshall.getAudioAttributes(), restoredConfig.getAudioAttributes()); |
| } |
| |
| public void testGetterMediaPlayer() throws Exception { |
| if (!isValidPlatform("testGetterMediaPlayer")) return; |
| if (hasAudioSilentProperty()) { |
| // No reasons to test since the started MediaPlayer will be muted and inactive |
| Log.w(TAG, "Skipping testGetterMediaPlayer"); |
| return; |
| } |
| |
| AudioManager am = new AudioManager(getContext()); |
| assertNotNull("Could not create AudioManager", am); |
| |
| final AudioAttributes aa = (new AudioAttributes.Builder()) |
| .setUsage(TEST_USAGE) |
| .setContentType(TEST_CONTENT) |
| .setAllowedCapturePolicy(ALLOW_CAPTURE_BY_ALL) |
| .build(); |
| |
| List<AudioPlaybackConfiguration> configs = am.getActivePlaybackConfigurations(); |
| final int nbActivePlayersBeforeStart = configs.size(); |
| |
| mMp = createPreparedMediaPlayer(R.raw.sine1khzs40dblong, aa, am.generateAudioSessionId()); |
| configs = am.getActivePlaybackConfigurations(); |
| assertEquals("inactive MediaPlayer, number of configs shouldn't have changed", |
| nbActivePlayersBeforeStart /*expected*/, configs.size()); |
| |
| mMp.start(); |
| Thread.sleep(TEST_TIMING_TOLERANCE_MS);// waiting for playback to start |
| configs = am.getActivePlaybackConfigurations(); |
| assertEquals("active MediaPlayer, number of configs should have increased", |
| nbActivePlayersBeforeStart + 1 /*expected*/, |
| configs.size()); |
| assertTrue("Active player, attributes not found", hasAttr(configs, aa)); |
| // verify "privileged" fields aren't available through reflection |
| final AudioPlaybackConfiguration config = configs.get(0); |
| final Class<?> confClass = config.getClass(); |
| final Method getClientUidMethod = confClass.getDeclaredMethod("getClientUid"); |
| final Method getClientPidMethod = confClass.getDeclaredMethod("getClientPid"); |
| final Method getPlayerTypeMethod = confClass.getDeclaredMethod("getPlayerType"); |
| final Method getSessionIdMethod = confClass.getDeclaredMethod("getSessionId"); |
| try { |
| Integer uid = (Integer) getClientUidMethod.invoke(config, (Object[]) null); |
| assertEquals("uid isn't protected", -1 /*expected*/, uid.intValue()); |
| Integer pid = (Integer) getClientPidMethod.invoke(config, (Object[]) null); |
| assertEquals("pid isn't protected", -1 /*expected*/, pid.intValue()); |
| Integer type = (Integer) getPlayerTypeMethod.invoke(config, (Object[]) null); |
| assertEquals("player type isn't protected", -1 /*expected*/, type.intValue()); |
| Integer sessionId = (Integer) getSessionIdMethod.invoke(config, (Object[]) null); |
| assertEquals("session ID isn't protected", 0 /*expected*/, sessionId.intValue()); |
| } catch (Exception e) { |
| fail("Exception thrown during reflection on config privileged fields"+ e); |
| } |
| } |
| |
| public void testCallbackMediaPlayer() throws Exception { |
| if (!isValidPlatform("testCallbackMediaPlayer")) return; |
| doTestCallbackMediaPlayer(false /* no custom Handler for callback */); |
| } |
| |
| public void testCallbackMediaPlayerHandler() throws Exception { |
| if (!isValidPlatform("testCallbackMediaPlayerHandler")) return; |
| doTestCallbackMediaPlayer(true /* use custom Handler for callback */); |
| } |
| |
| private void doTestCallbackMediaPlayer(boolean useHandlerInCallback) throws Exception { |
| final Handler h; |
| if (useHandlerInCallback) { |
| HandlerThread handlerThread = new HandlerThread(TAG); |
| handlerThread.start(); |
| h = new Handler(handlerThread.getLooper()); |
| } else { |
| h = null; |
| } |
| |
| AudioManager am = new AudioManager(getContext()); |
| assertNotNull("Could not create AudioManager", am); |
| |
| MyAudioPlaybackCallback callback = new MyAudioPlaybackCallback(); |
| |
| MyAudioPlaybackCallback registeredCallback = null; |
| |
| final AudioAttributes aa = (new AudioAttributes.Builder()) |
| .setUsage(TEST_USAGE) |
| .setContentType(TEST_CONTENT) |
| .build(); |
| |
| try { |
| mMp = createPreparedMediaPlayer(R.raw.sine1khzs40dblong, aa, |
| am.generateAudioSessionId()); |
| |
| am.registerAudioPlaybackCallback(callback, h /*handler*/); |
| registeredCallback = callback; |
| |
| // query how many active players before starting the MediaPlayer |
| List<AudioPlaybackConfiguration> configs = am.getActivePlaybackConfigurations(); |
| final int nbActivePlayersBeforeStart = configs.size(); |
| |
| assertPlayerStartAndCallbackWithPlayerAttributes(mMp, callback, |
| nbActivePlayersBeforeStart + 1, aa); |
| |
| |
| // stopping playback: callback is called with no match |
| callback.reset(); |
| mMp.pause(); |
| Thread.sleep(TEST_TIMING_TOLERANCE_MS); |
| |
| assertEquals("onPlaybackConfigChanged call count not expected after pause", |
| 1/*expected*/, callback.getCbInvocationNumber());//only 1 pause call since reset |
| assertEquals("number of active players not expected after pause", |
| nbActivePlayersBeforeStart/*expected*/, callback.getNbConfigs()); |
| |
| // unregister callback and start playback again |
| am.unregisterAudioPlaybackCallback(callback); |
| registeredCallback = null; |
| Thread.sleep(TEST_TIMING_TOLERANCE_MS); |
| callback.reset(); |
| mMp.start(); |
| Thread.sleep(TEST_TIMING_TOLERANCE_MS); |
| assertEquals("onPlaybackConfigChanged call count not expected after unregister", |
| 0/*expected*/, callback.getCbInvocationNumber()); //callback is unregistered |
| |
| // just call the callback once directly so it's marked as tested |
| final AudioManager.AudioPlaybackCallback apc = |
| (AudioManager.AudioPlaybackCallback) callback; |
| apc.onPlaybackConfigChanged(new ArrayList<AudioPlaybackConfiguration>()); |
| } finally { |
| if (registeredCallback != null) { |
| am.unregisterAudioPlaybackCallback(registeredCallback); |
| } |
| if (h != null) { |
| h.getLooper().quit(); |
| } |
| } |
| } |
| |
| public void testCallbackMediaPlayerRelease() throws Exception { |
| final HandlerThread handlerThread = new HandlerThread(TAG); |
| handlerThread.start(); |
| final Handler h = new Handler(handlerThread.getLooper()); |
| |
| AudioManager am = new AudioManager(getContext()); |
| assertNotNull("Could not create AudioManager", am); |
| |
| MyAudioPlaybackCallback callback = new MyAudioPlaybackCallback(); |
| |
| final AudioAttributes aa = (new AudioAttributes.Builder()) |
| .setUsage(TEST_USAGE) |
| .setContentType(TEST_CONTENT) |
| .build(); |
| |
| try { |
| mMp = createPreparedMediaPlayer(R.raw.sine1khzs40dblong, aa, |
| am.generateAudioSessionId()); |
| |
| am.registerAudioPlaybackCallback(callback, h /*handler*/); |
| |
| // query how many active players before starting the MediaPlayer |
| List<AudioPlaybackConfiguration> configs = |
| am.getActivePlaybackConfigurations(); |
| final int nbActivePlayersBeforeStart = configs.size(); |
| |
| assertPlayerStartAndCallbackWithPlayerAttributes(mMp, callback, |
| nbActivePlayersBeforeStart + 1, aa); |
| |
| // release the player without stopping or pausing it first |
| callback.reset(); |
| mMp.release(); |
| Thread.sleep(TEST_TIMING_TOLERANCE_MS); |
| |
| assertEquals("onPlaybackConfigChanged call count not expected after release", |
| 1/*expected*/, callback.getCbInvocationNumber());//only release call since reset |
| assertEquals("number of active players not expected after release", |
| nbActivePlayersBeforeStart/*expected*/, callback.getNbConfigs()); |
| |
| } finally { |
| am.unregisterAudioPlaybackCallback(callback); |
| if (h != null) { |
| h.getLooper().quit(); |
| } |
| } |
| } |
| |
| public void testGetterSoundPool() throws Exception { |
| if (!isValidPlatform("testSoundPool")) return; |
| |
| AudioManager am = new AudioManager(getContext()); |
| assertNotNull("Could not create AudioManager", am); |
| |
| MyAudioPlaybackCallback callback = new MyAudioPlaybackCallback(); |
| am.registerAudioPlaybackCallback(callback, null /*handler*/); |
| |
| // query how many active players before starting the SoundPool |
| List<AudioPlaybackConfiguration> configs = am.getActivePlaybackConfigurations(); |
| int nbActivePlayersBeforeStart = 0; |
| for (AudioPlaybackConfiguration apc : configs) { |
| if (apc.getPlayerState() == AudioPlaybackConfiguration.PLAYER_STATE_STARTED) { |
| nbActivePlayersBeforeStart++; |
| } |
| } |
| |
| final AudioAttributes aa = (new AudioAttributes.Builder()) |
| .setUsage(TEST_USAGE) |
| .setContentType(TEST_CONTENT) |
| .setAllowedCapturePolicy(ALLOW_CAPTURE_BY_SYSTEM) |
| .build(); |
| mSp = createSoundPool(aa); |
| playSoundPool(mSp, getContext()); |
| |
| Thread.sleep(TEST_TIMING_TOLERANCE_MS); |
| |
| mSp.autoPause(); |
| Thread.sleep(TEST_TIMING_TOLERANCE_MS); |
| // query how many active players after pausing |
| configs = am.getActivePlaybackConfigurations(); |
| int nbActivePlayersAfterPause = 0; |
| for (AudioPlaybackConfiguration apc : configs) { |
| if (apc.getPlayerState() == AudioPlaybackConfiguration.PLAYER_STATE_STARTED) { |
| nbActivePlayersAfterPause++; |
| } |
| } |
| assertEquals("Number of active players changed after pausing SoundPool", |
| nbActivePlayersBeforeStart, nbActivePlayersAfterPause); |
| } |
| |
| @ApiTest(apis = {"android.media.AudioManager#getActivePlaybackConfigurations", |
| "android.media.AudioManager.AudioPlaybackCallback#onPlaybackConfigChanged"}) |
| public void testGetterAndCallbackConsistency() throws Exception { |
| if (!isValidPlatform("testGetterAndCallbackConsistency")) return; |
| |
| AudioManager am = new AudioManager(getContext()); |
| assertNotNull("Could not create AudioManager", am); |
| |
| MyAudioPlaybackCallback callback = new MyAudioPlaybackCallback(); |
| am.registerAudioPlaybackCallback(callback, null /*handler*/); |
| |
| final AudioAttributes aa = (new AudioAttributes.Builder()) |
| .setUsage(TEST_USAGE) |
| .setContentType(TEST_CONTENT) |
| .setAllowedCapturePolicy(ALLOW_CAPTURE_BY_SYSTEM) |
| .build(); |
| mSp = createSoundPool(aa); |
| mMp = createPreparedMediaPlayer(R.raw.sine1khzs40dblong, aa, |
| am.generateAudioSessionId()); |
| |
| try { |
| playSoundPool(mSp, getContext()); |
| |
| callback.reset(); |
| mMp.start(); |
| |
| assertTrue("onPlaybackConfigChanged should have been called for start and new device", |
| callback.waitForCallbacks(2, |
| TEST_TIMING_TOLERANCE_MS + PLAY_ROUTING_TIMING_TOLERANCE_MS)); |
| assertListsAreConsistent(am.getActivePlaybackConfigurations(), callback.getConfigs()); |
| |
| mSp.autoPause(); |
| mMp.stop(); |
| } finally { |
| am.unregisterAudioPlaybackCallback(callback); |
| } |
| } |
| |
| public void testGetAudioDeviceInfoMediaPlayerStart() throws Exception { |
| if (!isValidPlatform("testGetAudioDeviceInfoMediaPlayerStart")) return; |
| |
| final HandlerThread handlerThread = new HandlerThread(TAG); |
| handlerThread.start(); |
| final Handler h = new Handler(handlerThread.getLooper()); |
| |
| AudioManager am = new AudioManager(getContext()); |
| assertNotNull("Could not create AudioManager", am); |
| |
| MyAudioPlaybackCallback callback = new MyAudioPlaybackCallback(); |
| |
| final AudioAttributes aa = (new AudioAttributes.Builder()) |
| .setUsage(TEST_USAGE) |
| .setContentType(TEST_CONTENT) |
| .build(); |
| |
| try { |
| mMp = createPreparedMediaPlayer(R.raw.sine1khzs40dblong, aa, |
| am.generateAudioSessionId()); |
| |
| am.registerAudioPlaybackCallback(callback, h /*handler*/); |
| |
| // query how many active players before starting the MediaPlayer |
| List<AudioPlaybackConfiguration> configs = |
| am.getActivePlaybackConfigurations(); |
| final int nbActivePlayersBeforeStart = configs.size(); |
| |
| assertPlayerStartAndCallbackWithPlayerAttributes(mMp, callback, |
| nbActivePlayersBeforeStart + 1, aa); |
| |
| assertTrue("Active player, device not found", |
| hasDevice(callback.getConfigs(), aa)); |
| |
| } finally { |
| am.unregisterAudioPlaybackCallback(callback); |
| if (h != null) { |
| h.getLooper().quit(); |
| } |
| } |
| } |
| |
| @ApiTest(apis = {"android.media.AudioManager#getActivePlaybackConfigurations", |
| "android.media.AudioManager.AudioPlaybackCallback#onPlaybackConfigChanged", |
| "android.media.AudioManager.AudioPlaybackCallback#isMuted", |
| "android.media.AudioManager.AudioPlaybackCallback#getMutedBy"}) |
| public void testAudioTrackMuteFromAppOpsNotification() throws Exception { |
| if (!isValidPlatform("testAudioTrackMuteFromAppOpsNotification")) return; |
| if (hasAudioSilentProperty()) { |
| Log.w(TAG, "Skipping testAudioTrackMuteFromAppOpsNotification"); |
| return; |
| } |
| |
| initializeAudioTrack(); |
| checkMuteFromAppOpsNotification(new MyPlayer(mAt)); |
| } |
| |
| @ApiTest(apis = {"android.media.AudioManager#getActivePlaybackConfigurations", |
| "android.media.AudioManager.AudioPlaybackCallback#onPlaybackConfigChanged", |
| "android.media.AudioManager.AudioPlaybackCallback#isMuted", |
| "android.media.AudioManager.AudioPlaybackCallback#getMutedBy"}) |
| public void testMediaPlayerMuteFromAppOpsNotification() throws Exception { |
| if (!isValidPlatform("testMediaPlayerMuteFromAppOpsNotification")) return; |
| if (hasAudioSilentProperty()) { |
| Log.w(TAG, "Skipping testMediaPlayerMuteFromAppOpsNotification"); |
| return; |
| } |
| |
| initializeMediaPlayer(); |
| checkMuteFromAppOpsNotification(new MyPlayer(mMp)); |
| } |
| |
| private void checkMuteFromAppOpsNotification(MyPlayer player) throws Exception { |
| verifyMuteUnmuteNotifications(/*start=*/player.mPlay, |
| /*mute=*/() -> { |
| try { |
| setOpMode(getContext().getPackageName(), OPSTR_PLAY_AUDIO, MODE_IGNORED); |
| } catch (IOException e) { |
| fail("Failed to set AppOps ignore for play audio: " + e); |
| } |
| }, |
| /*unmute=*/() -> { |
| try { |
| if (getOpMode(getContext().getPackageName(), OPSTR_PLAY_AUDIO) |
| != MODE_ALLOWED) { |
| setOpMode(getContext().getPackageName(), OPSTR_PLAY_AUDIO, |
| MODE_ALLOWED); |
| } |
| } catch (IOException e) { |
| fail("Failed to set AppOps allow for play audio: " + e); |
| } |
| }, /*muteChangesActiveState=*/true, MUTED_BY_APP_OPS); |
| } |
| |
| @ApiTest(apis = {"android.media.AudioManager#getActivePlaybackConfigurations", |
| "android.media.AudioManager.AudioPlaybackCallback#onPlaybackConfigChanged", |
| "android.media.AudioManager.AudioPlaybackCallback#isMuted", |
| "android.media.AudioManager.AudioPlaybackCallback#getMutedBy"}) |
| public void testAudioTrackMuteFromStreamVolumeNotification() throws Exception { |
| if (!isValidPlatform("testAudioTrackMuteFromStreamVolumeNotification")) return; |
| if (hasAudioSilentProperty()) { |
| Log.w(TAG, "Skipping testAudioTrackMuteFromStreamVolumeNotification"); |
| return; |
| } |
| |
| initializeAudioTrack(); |
| checkMuteFromStreamVolumeNotification(new MyPlayer(mAt)); |
| } |
| |
| @ApiTest(apis = {"android.media.AudioManager#getActivePlaybackConfigurations", |
| "android.media.AudioManager.AudioPlaybackCallback#onPlaybackConfigChanged", |
| "android.media.AudioManager.AudioPlaybackCallback#isMuted", |
| "android.media.AudioManager.AudioPlaybackCallback#getMutedBy"}) |
| public void testMediaPlayerMuteFromStreamVolumeNotification() throws Exception { |
| if (!isValidPlatform("testMediaPlayerMuteFromStreamVolumeNotification")) return; |
| if (hasAudioSilentProperty()) { |
| Log.w(TAG, "Skipping testMediaPlayerMuteFromStreamVolumeNotification"); |
| return; |
| } |
| |
| initializeMediaPlayer(); |
| checkMuteFromStreamVolumeNotification(new MyPlayer(mMp)); |
| } |
| |
| private void checkMuteFromStreamVolumeNotification(MyPlayer player) throws Exception { |
| AudioManager am = new AudioManager(getContext()); |
| assertNotNull("Could not create AudioManager", am); |
| |
| verifyMuteUnmuteNotifications(/*start=*/player.mPlay, |
| /*mute=*/ |
| () -> am.adjustStreamVolume(STREAM_NOTIFICATION, ADJUST_MUTE, /* flags= */0), |
| /*unmute=*/ |
| () -> am.adjustStreamVolume(STREAM_NOTIFICATION, ADJUST_UNMUTE, /* flags= */0), |
| /*muteChangesActiveState=*/false, MUTED_BY_STREAM_VOLUME); |
| } |
| |
| @ApiTest(apis = {"android.media.AudioManager#getActivePlaybackConfigurations", |
| "android.media.AudioManager.AudioPlaybackCallback#onPlaybackConfigChanged", |
| "android.media.AudioManager.AudioPlaybackCallback#isMuted", |
| "android.media.AudioManager.AudioPlaybackCallback#getMutedBy"}) |
| public void testAudioTrackMuteFromClientVolumeNotification() throws Exception { |
| if (!isValidPlatform("testAudioTrackMuteFromClientVolumeNotification")) return; |
| if (hasAudioSilentProperty()) { |
| Log.w(TAG, "Skipping testAudioTrackMuteFromClientVolumeNotification"); |
| return; |
| } |
| |
| initializeAudioTrack(); |
| checkMuteFromClientVolumeNotification(new MyPlayer(mAt)); |
| } |
| |
| |
| @ApiTest(apis = {"android.media.AudioManager#getActivePlaybackConfigurations", |
| "android.media.AudioManager.AudioPlaybackCallback#onPlaybackConfigChanged", |
| "android.media.AudioManager.AudioPlaybackCallback#isMuted", |
| "android.media.AudioManager.AudioPlaybackCallback#getMutedBy"}) |
| public void testMediaPlayerMuteFromClientVolumeNotification() throws Exception { |
| if (!isValidPlatform("testMediaPlayerMuteFromClientVolumeNotification")) return; |
| if (hasAudioSilentProperty()) { |
| Log.w(TAG, "Skipping testMediaPlayerMuteFromClientVolumeNotification"); |
| return; |
| } |
| |
| initializeMediaPlayer(); |
| checkMuteFromClientVolumeNotification(new MyPlayer(mMp)); |
| } |
| |
| private void checkMuteFromClientVolumeNotification(MyPlayer player) throws Exception { |
| verifyMuteUnmuteNotifications(/*start=*/player.mPlay, |
| /*mute=*/() -> player.mSetClientVolume.accept(0.f), |
| /*unmute=*/() -> player.mSetClientVolume.accept(1.f), |
| /*muteChangesActiveState=*/true, MUTED_BY_CLIENT_VOLUME); |
| } |
| |
| @ApiTest(apis = {"android.media.AudioManager#getActivePlaybackConfigurations", |
| "android.media.AudioManager.AudioPlaybackCallback#onPlaybackConfigChanged", |
| "android.media.AudioManager.AudioPlaybackCallback#isMuted", |
| "android.media.AudioManager.AudioPlaybackCallback#getMutedBy"}) |
| public void testAudioTrackMuteFromVolumeShaperNotification() throws Exception { |
| if (!isValidPlatform("testAudioTrackMuteFromVolumeShaperNotification")) return; |
| if (hasAudioSilentProperty()) { |
| Log.w(TAG, "Skipping testAudioTrackMuteFromVolumeShaperNotification"); |
| return; |
| } |
| |
| initializeAudioTrack(); |
| checkMuteFromVolumeShaperNotification(new MyPlayer(mAt)); |
| } |
| |
| @ApiTest(apis = {"android.media.AudioManager#getActivePlaybackConfigurations", |
| "android.media.AudioManager.AudioPlaybackCallback#onPlaybackConfigChanged", |
| "android.media.AudioManager.AudioPlaybackCallback#isMuted", |
| "android.media.AudioManager.AudioPlaybackCallback#getMutedBy"}) |
| public void testMediaPlayerMuteFromVolumeShaperNotification() throws Exception { |
| if (!isValidPlatform("testMediaPlayerMuteFromVolumeShaperNotification")) return; |
| if (hasAudioSilentProperty()) { |
| Log.w(TAG, "Skipping testMediaPlayerMuteFromVolumeShaperNotification"); |
| return; |
| } |
| |
| initializeMediaPlayer(); |
| checkMuteFromVolumeShaperNotification(new MyPlayer(mMp)); |
| } |
| |
| private void checkMuteFromVolumeShaperNotification(MyPlayer player) throws Exception { |
| verifyMuteUnmuteNotifications(/*start=*/player.mPlay, |
| /*mute=*/() -> { |
| mMuteShaper = player.mCreateVolumeShaper.apply(SHAPER_MUTE); |
| mMuteShaper.apply(VolumeShaper.Operation.PLAY); |
| }, |
| /*unmute=*/() -> { |
| mMuteShaper.replace(SHAPER_MUTE, VolumeShaper.Operation.REVERSE, /*join=*/ |
| false); |
| mMuteShaper.apply(VolumeShaper.Operation.PLAY); |
| }, /*muteChangesActiveState=*/true, MUTED_BY_VOLUME_SHAPER); |
| } |
| |
| private void verifyMuteUnmuteNotifications(Runnable start, Runnable mute, Runnable unmute, |
| boolean muteChangesActiveState, int checkFlag) |
| throws Exception { |
| AudioManager am = new AudioManager(getContext()); |
| assertNotNull("Could not create AudioManager", am); |
| |
| MyAudioPlaybackCallback callback = new MyAudioPlaybackCallback(); |
| |
| try { |
| am.registerAudioPlaybackCallback(callback, null /*handler*/); |
| |
| // query how many active players before starting the MediaPlayer |
| final int nbActivePlayersBeforeStart = am.getActivePlaybackConfigurations().size(); |
| |
| // start playing audio |
| start.run(); |
| |
| if (muteChangesActiveState) { |
| assertTrue("onPlaybackConfigChanged new player, device expected", |
| callback.waitForCallbacks(2, |
| TEST_TIMING_TOLERANCE_MS + PLAY_ROUTING_TIMING_TOLERANCE_MS)); |
| } else { |
| Thread.sleep(TEST_TIMING_TOLERANCE_MS + PLAY_ROUTING_TIMING_TOLERANCE_MS); |
| } |
| |
| // mute with Runnable |
| callback.reset(); |
| mute.run(); |
| |
| if (muteChangesActiveState) { |
| assertTrue("onPlaybackConfigChanged for mute expected", |
| callback.waitForCallbacks(1, |
| TEST_TIMING_TOLERANCE_MS + PLAY_ROUTING_TIMING_TOLERANCE_MS)); |
| } else { |
| Thread.sleep(TEST_TIMING_TOLERANCE_MS + PLAY_ROUTING_TIMING_TOLERANCE_MS); |
| } |
| |
| checkMutedApi(checkFlag); |
| |
| assertEquals("number of active players after mute not expected", |
| nbActivePlayersBeforeStart + (muteChangesActiveState ? 0 : 1), |
| am.getActivePlaybackConfigurations().size()); |
| |
| // unmute with Runnable |
| callback.reset(); |
| unmute.run(); |
| |
| if (muteChangesActiveState) { |
| assertTrue("onPlaybackConfigChanged for unmute expected", |
| callback.waitForCallbacks(1, |
| TEST_TIMING_TOLERANCE_MS + PLAY_ROUTING_TIMING_TOLERANCE_MS)); |
| } else { |
| Thread.sleep(TEST_TIMING_TOLERANCE_MS + PLAY_ROUTING_TIMING_TOLERANCE_MS); |
| } |
| |
| assertEquals("number of active players after unmute not expected", |
| nbActivePlayersBeforeStart + 1, |
| am.getActivePlaybackConfigurations().size()); |
| } finally { |
| am.unregisterAudioPlaybackCallback(callback); |
| unmute.run(); |
| } |
| } |
| |
| private void checkMutedApi(int checkFlag) { |
| InstrumentationRegistry.getInstrumentation().getUiAutomation().adoptShellPermissionIdentity( |
| Manifest.permission.MODIFY_AUDIO_ROUTING); |
| |
| AudioPlaybackConfiguration currentConfiguration = findConfiguration(); |
| assertTrue("APC should be muted", currentConfiguration.isMuted()); |
| assertEquals("APC muted by wrong source", currentConfiguration.getMutedBy(), checkFlag); |
| |
| InstrumentationRegistry.getInstrumentation().getUiAutomation() |
| .dropShellPermissionIdentity(); |
| } |
| |
| private AudioPlaybackConfiguration findConfiguration() { |
| int uid; |
| Context context = getContext(); |
| try { |
| uid = context.getPackageManager().getApplicationInfo(context.getPackageName(), |
| PackageManager.ApplicationInfoFlags.of(0)).uid; |
| } catch (PackageManager.NameNotFoundException e) { |
| uid = -1; |
| } |
| |
| AudioManager am = new AudioManager(getContext()); |
| List<AudioPlaybackConfiguration> configList = am.getActivePlaybackConfigurations(); |
| AudioPlaybackConfiguration result = null; |
| for (AudioPlaybackConfiguration config : configList) { |
| if (config.getClientUid() == uid) { |
| Log.v(TAG, |
| "AudioPlaybackConfiguration " + config + " uid " + config.getClientUid()); |
| result = config; |
| } |
| } |
| assertNotNull("Could not find AudioPlaybackConfiguration for uid " + uid, result); |
| return result; |
| } |
| |
| private void initializeAudioTrack() { |
| final int bufferSizeInBytes = |
| TEST_AUDIO_TRACK_PLAY_SECONDS * TEST_AUDIO_TRACK_SAMPLERATE |
| * TEST_AUDIO_TRACK_CHANNELS; |
| |
| ByteBuffer audioData = createSoundDataInShortByteBuffer(bufferSizeInBytes, |
| TEST_AUDIO_TRACK_SAMPLERATE, TEST_AUDIO_TRACK_FREQUENCY, |
| TEST_AUDIO_TRACK_SWEEP); |
| |
| final AudioAttributes aa = (new AudioAttributes.Builder()) |
| .setUsage(TEST_USAGE) |
| .setContentType(TEST_CONTENT) |
| .build(); |
| |
| mAt = new AudioTrack.Builder() |
| .setAudioAttributes(aa) |
| .setAudioFormat(new AudioFormat.Builder() |
| .setSampleRate(TEST_AUDIO_TRACK_SAMPLERATE) |
| .setChannelMask(AudioFormat.CHANNEL_OUT_STEREO) |
| .setEncoding(AudioFormat.ENCODING_PCM_16BIT) |
| .build()) |
| .setBufferSizeInBytes(bufferSizeInBytes) |
| .build(); |
| |
| mAt.write(audioData, audioData.remaining(), WRITE_NON_BLOCKING); |
| } |
| |
| private void initializeMediaPlayer() throws Exception { |
| AudioManager am = new AudioManager(getContext()); |
| assertNotNull("Could not create AudioManager", am); |
| |
| final AudioAttributes aa = (new AudioAttributes.Builder()) |
| .setUsage(TEST_USAGE) |
| .setContentType(TEST_CONTENT) |
| .build(); |
| |
| mMp = createPreparedMediaPlayer(R.raw.sine1khzs40dblong, aa, |
| am.generateAudioSessionId()); |
| } |
| |
| private @Nullable MediaPlayer createPreparedMediaPlayer( |
| @RawRes int resID, AudioAttributes aa, int session) throws Exception { |
| final TestUtils.Monitor onPreparedCalled = new TestUtils.Monitor(); |
| final MediaPlayer mp = createPlayer(resID, aa, session); |
| mp.setOnPreparedListener(mp1 -> onPreparedCalled.signal()); |
| mp.prepare(); |
| onPreparedCalled.waitForSignal(MEDIAPLAYER_PREPARE_TIMEOUT_MS); |
| assertTrue( |
| "MediaPlayer wasn't prepared in under " + MEDIAPLAYER_PREPARE_TIMEOUT_MS + " ms", |
| onPreparedCalled.isSignalled()); |
| return mp; |
| } |
| |
| private MediaPlayer createPlayer( |
| @RawRes int resID, AudioAttributes aa, int session) throws IOException { |
| MediaPlayer mp = new MediaPlayer(); |
| mp.setAudioAttributes(aa); |
| mp.setAudioSessionId(session); |
| AssetFileDescriptor afd = getContext().getResources().openRawResourceFd(resID); |
| try { |
| mp.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength()); |
| } finally { |
| afd.close(); |
| } |
| return mp; |
| } |
| |
| private static SoundPool createSoundPool(AudioAttributes aa) { |
| return new SoundPool.Builder() |
| .setAudioAttributes(aa) |
| .setMaxStreams(1) |
| .build(); |
| } |
| |
| /** Loads a track and plays it with the passed {@link SoundPool}. */ |
| private static void playSoundPool(SoundPool sp, Context context) throws InterruptedException { |
| final Object loadLock = new Object(); |
| final SoundPool zepool = sp; |
| // load a sound and play it once load completion is reported |
| sp.setOnLoadCompleteListener(new SoundPool.OnLoadCompleteListener() { |
| @Override |
| public void onLoadComplete(SoundPool soundPool, int sampleId, int status) { |
| assertEquals("Receiving load completion for wrong SoundPool", zepool, sp); |
| assertEquals("Load completion error", 0 /*success expected*/, status); |
| synchronized (loadLock) { |
| loadLock.notify(); |
| } |
| } |
| }); |
| final int loadId = sp.load(context, R.raw.sine1320hz5sec, 1/*priority*/); |
| synchronized (loadLock) { |
| loadLock.wait(TEST_TIMEOUT_SOUNDPOOL_LOAD_MS); |
| } |
| |
| int res = sp.play(loadId, 1.0f /*leftVolume*/, 1.0f /*rightVolume*/, 1 /*priority*/, |
| 0 /*loop*/, 1.0f/*rate*/); |
| // FIXME SoundPool activity is not reported yet, but exercise creation/release with |
| // an AudioPlaybackCallback registered |
| assertTrue("Error playing sound through SoundPool", res > 0); |
| } |
| |
| private static void assertListsAreConsistent(List<AudioPlaybackConfiguration> config1, |
| List<AudioPlaybackConfiguration> config2) { |
| assertEquals("Different size of audio playback configurations reported", config1.size(), |
| config2.size()); |
| assertTrue("Reported audio playback configurations are inconsistent", |
| config1.containsAll(config2) && config2.containsAll(config1)); |
| } |
| |
| private void assertPlayerStartAndCallbackWithPlayerAttributes( |
| MediaPlayer mp, MyAudioPlaybackCallback callback, |
| int activePlayerCount, AudioAttributes aa) throws Exception{ |
| mp.start(); |
| |
| assertTrue("onPlaybackConfigChanged play and device called expected " |
| , callback.waitForCallbacks(2, |
| TEST_TIMING_TOLERANCE_MS + PLAY_ROUTING_TIMING_TOLERANCE_MS)); |
| assertEquals("number of active players not expected", |
| // one more player active |
| activePlayerCount/*expected*/, callback.getNbConfigs()); |
| assertTrue("Active player, attributes not found", hasAttr(callback.getConfigs(), aa)); |
| } |
| |
| private static class MyAudioPlaybackCallback extends AudioManager.AudioPlaybackCallback { |
| private final Object mCbLock = new Object(); |
| @GuardedBy("mCbLock") |
| private int mCalled; |
| @GuardedBy("mCbLock") |
| private List<AudioPlaybackConfiguration> mConfigs; |
| |
| final TestUtils.Monitor mOnCalledMonitor = new TestUtils.Monitor(); |
| |
| void reset() { |
| synchronized (mCbLock) { |
| mCalled = 0; |
| mConfigs = new ArrayList<AudioPlaybackConfiguration>(); |
| } |
| mOnCalledMonitor.reset(); |
| } |
| |
| int getCbInvocationNumber() { |
| synchronized (mCbLock) { |
| return mCalled; |
| } |
| } |
| |
| int getNbConfigs() { |
| return getConfigs().size(); |
| } |
| |
| List<AudioPlaybackConfiguration> getConfigs() { |
| synchronized (mCbLock) { |
| return mConfigs; |
| } |
| } |
| |
| MyAudioPlaybackCallback() { |
| reset(); |
| } |
| |
| @Override |
| public void onPlaybackConfigChanged(List<AudioPlaybackConfiguration> configs) { |
| synchronized (mCbLock) { |
| mCalled++; |
| mConfigs = configs; |
| } |
| mOnCalledMonitor.signal(); |
| } |
| |
| public boolean waitForCallbacks(int calledCount, long timeoutMs) |
| throws InterruptedException { |
| int signalsCounted = |
| mOnCalledMonitor.waitForCountedSignals(calledCount, timeoutMs); |
| return (signalsCounted == calledCount); |
| } |
| } |
| |
| private static class MyPlayer { |
| Runnable mPlay; |
| Consumer<Float> mSetClientVolume; |
| Function<VolumeShaper.Configuration, VolumeShaper> mCreateVolumeShaper; |
| |
| MyPlayer(AudioTrack at) { |
| mPlay = at::play; |
| mSetClientVolume = at::setVolume; |
| mCreateVolumeShaper = at::createVolumeShaper; |
| } |
| |
| MyPlayer(MediaPlayer mp) { |
| mPlay = mp::start; |
| mSetClientVolume = mp::setVolume; |
| mCreateVolumeShaper = mp::createVolumeShaper; |
| } |
| } |
| |
| private static boolean hasAttr(List<AudioPlaybackConfiguration> configs, AudioAttributes aa) { |
| for (AudioPlaybackConfiguration apc : configs) { |
| if (apc.getAudioAttributes().getContentType() == aa.getContentType() |
| && apc.getAudioAttributes().getUsage() == aa.getUsage() |
| && apc.getAudioAttributes().getFlags() == aa.getFlags() |
| && anonymizeCapturePolicy(apc.getAudioAttributes().getAllowedCapturePolicy()) |
| == aa.getAllowedCapturePolicy()) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| private static boolean hasDevice(List<AudioPlaybackConfiguration> configs, AudioAttributes aa) { |
| for (AudioPlaybackConfiguration apc : configs) { |
| if (apc.getAudioAttributes().getContentType() == aa.getContentType() |
| && apc.getAudioAttributes().getUsage() == aa.getUsage() |
| && apc.getAudioAttributes().getFlags() == aa.getFlags() |
| && anonymizeCapturePolicy(apc.getAudioAttributes().getAllowedCapturePolicy()) |
| == aa.getAllowedCapturePolicy() |
| && apc.getAudioDeviceInfo() != null) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /** ALLOW_CAPTURE_BY_SYSTEM is anonymized to ALLOW_CAPTURE_BY_NONE. */ |
| @CapturePolicy |
| private static int anonymizeCapturePolicy(@CapturePolicy int policy) { |
| if (policy == ALLOW_CAPTURE_BY_SYSTEM) { |
| return ALLOW_CAPTURE_BY_NONE; |
| } |
| return policy; |
| } |
| |
| private boolean isValidPlatform(String testName) { |
| if (!getContext().getPackageManager() |
| .hasSystemFeature(PackageManager.FEATURE_AUDIO_OUTPUT)) { |
| Log.w(TAG,"AUDIO_OUTPUT feature not found. This system might not have a valid " |
| + "audio output HAL, skipping test " + testName); |
| return false; |
| } |
| return true; |
| } |
| |
| private static boolean hasAudioSilentProperty() { |
| String silent = null; |
| |
| try { |
| silent = (String) Class.forName("android.os.SystemProperties").getMethod("get", |
| String.class).invoke(null, "ro.audio.silent"); |
| } catch (Exception e) { |
| Log.e(TAG, "Could not invoke SystemProperties.get(ro.audio.silent)", e); |
| } |
| |
| if (silent != null && silent.equals("1")) { |
| Log.w(TAG, "Device has ro.audio.silent set"); |
| return true; |
| } |
| |
| return false; |
| } |
| } |