blob: 7667922abd1d4d92120c8c0e7651881c93a4fc66 [file] [log] [blame]
/*
* Copyright (C) 2021 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.cts;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.junit.Assume.assumeTrue;
import static org.testng.Assert.assertThrows;
import android.content.pm.PackageManager;
import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.MediaSyncEvent;
import android.util.Log;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SdkSuppress;
import androidx.test.runner.AndroidJUnit4;
import com.android.compatibility.common.util.SystemUtil;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.IOException;
@NonMediaMainlineTest
@RunWith(AndroidJUnit4.class)
@SdkSuppress(minSdkVersion = 31, codeName = "S")
public class AudioRecordSharedAudioTest {
private static final String TAG = "AudioRecordSharedAudioTest";
private static final int SAMPLING_RATE_HZ = 16000;
@Before
public void setUp() throws Exception {
assumeTrue(hasMicrophone());
InstrumentationRegistry.getInstrumentation().getUiAutomation()
.adoptShellPermissionIdentity();
clearAudioserverPermissionCache();
}
@After
public void tearDown() throws Exception {
InstrumentationRegistry.getInstrumentation().getUiAutomation()
.dropShellPermissionIdentity();
clearAudioserverPermissionCache();
}
@Test
public void testPermissionFailure() throws Exception {
InstrumentationRegistry.getInstrumentation().getUiAutomation()
.dropShellPermissionIdentity();
clearAudioserverPermissionCache();
assertThrows(UnsupportedOperationException.class, () -> {
AudioRecord record = new AudioRecord.Builder().setMaxSharedAudioHistoryMillis(
AudioRecord.getMaxSharedAudioHistoryMillis() - 1).build();
});
final AudioRecord record =
new AudioRecord.Builder()
.setAudioFormat(new AudioFormat.Builder()
.setSampleRate(SAMPLING_RATE_HZ)
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
.setChannelMask(AudioFormat.CHANNEL_IN_MONO).build())
.setBufferSizeInBytes(SAMPLING_RATE_HZ
* AudioFormat.getBytesPerSample(AudioFormat.ENCODING_PCM_16BIT))
.build();
assertEquals(AudioRecord.STATE_INITIALIZED, record.getState());
record.startRecording();
Thread.sleep(500);
assertThrows(SecurityException.class, () -> {
record.shareAudioHistory(
InstrumentationRegistry.getTargetContext().getPackageName(), 100);
});
record.stop();
record.release();
}
@Test
public void testPermissionSuccess() throws Exception {
AudioRecord record = new AudioRecord.Builder().setAudioFormat(new AudioFormat.Builder()
.setSampleRate(SAMPLING_RATE_HZ)
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
.setChannelMask(AudioFormat.CHANNEL_IN_MONO).build())
.setBufferSizeInBytes(SAMPLING_RATE_HZ
* AudioFormat.getBytesPerSample(AudioFormat.ENCODING_PCM_16BIT))
.setMaxSharedAudioHistoryMillis(
AudioRecord.getMaxSharedAudioHistoryMillis()-1)
.build();
assertEquals(AudioRecord.STATE_INITIALIZED, record.getState());
record.startRecording();
Thread.sleep(500);
try {
record.shareAudioHistory(
InstrumentationRegistry.getTargetContext().getPackageName(), 100);
} catch (SecurityException e) {
fail("testPermissionSuccess shareAudioHistory be allowed");
} finally {
record.stop();
record.release();
}
}
@Test
public void testBadValues() throws Exception {
assertThrows(IllegalArgumentException.class, () -> {
AudioRecord.Builder builder = new AudioRecord.Builder()
.setMaxSharedAudioHistoryMillis(
AudioRecord.getMaxSharedAudioHistoryMillis() + 1);
});
assertThrows(IllegalArgumentException.class, () -> {
AudioRecord.Builder builder = new AudioRecord.Builder()
.setMaxSharedAudioHistoryMillis(-1);
});
final AudioRecord record =
new AudioRecord.Builder().setAudioFormat(new AudioFormat.Builder()
.setSampleRate(SAMPLING_RATE_HZ)
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
.setChannelMask(AudioFormat.CHANNEL_IN_MONO).build())
.setBufferSizeInBytes(SAMPLING_RATE_HZ
* AudioFormat.getBytesPerSample(AudioFormat.ENCODING_PCM_16BIT))
.setMaxSharedAudioHistoryMillis(
AudioRecord.getMaxSharedAudioHistoryMillis()-1)
.build();
assertEquals(AudioRecord.STATE_INITIALIZED, record.getState());
record.startRecording();
Thread.sleep(500);
assertThrows(NullPointerException.class, () -> {
record.shareAudioHistory(null /* sharedPackage */, 100 /* startFromMillis */);
});
assertThrows(IllegalArgumentException.class, () -> {
record.shareAudioHistory(
InstrumentationRegistry.getTargetContext().getPackageName(),
-1 /* startFromMillis */);
});
record.stop();
record.release();
}
@Test
public void testCapturesMatch() throws Exception {
AudioRecord record1 = null;
AudioRecord record2 = null;
try {
record1 = new AudioRecord.Builder().setAudioFormat(new AudioFormat.Builder()
.setSampleRate(SAMPLING_RATE_HZ)
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
.setChannelMask(AudioFormat.CHANNEL_IN_MONO).build())
.setBufferSizeInBytes(SAMPLING_RATE_HZ
* AudioFormat.getBytesPerSample(AudioFormat.ENCODING_PCM_16BIT))
.setMaxSharedAudioHistoryMillis(
AudioRecord.getMaxSharedAudioHistoryMillis() - 1)
.build();
assertEquals(AudioRecord.STATE_INITIALIZED, record1.getState());
record1.startRecording();
final int RECORD1_NUM_SAMPLES = SAMPLING_RATE_HZ / 2;
short[] buffer1 = new short[RECORD1_NUM_SAMPLES];
// blocking read should allow for at least 500ms of audio in buffer
int samplesRead = record1.read(buffer1, 0, RECORD1_NUM_SAMPLES);
assertTrue(samplesRead >= RECORD1_NUM_SAMPLES);
final int RECORD2_START_TIME_MS = 100;
MediaSyncEvent event = record1.shareAudioHistory(
InstrumentationRegistry.getTargetContext().getPackageName(),
(long) RECORD2_START_TIME_MS /* startFromMillis */);
assertEquals(event.getAudioSessionId(), record1.getAudioSessionId());
record2 = new AudioRecord.Builder().setAudioFormat(new AudioFormat.Builder()
.setSampleRate(SAMPLING_RATE_HZ)
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
.setChannelMask(AudioFormat.CHANNEL_IN_MONO).build())
.setBufferSizeInBytes(SAMPLING_RATE_HZ
* AudioFormat.getBytesPerSample(AudioFormat.ENCODING_PCM_16BIT))
.setSharedAudioEvent(event)
.build();
assertEquals(AudioRecord.STATE_INITIALIZED, record2.getState());
record2.startRecording();
final int RECORD2_NUM_SAMPLES = SAMPLING_RATE_HZ / 5;
short[] buffer2 = new short[RECORD2_NUM_SAMPLES];
samplesRead = record2.read(buffer2, 0, RECORD2_NUM_SAMPLES);
assertTrue(samplesRead >= RECORD2_NUM_SAMPLES);
record2.stop();
record1.stop();
// verify that the audio read by 2nd AudioRecord exactly matches the audio read
// by 1st AudioRecord starting from the expected start time with a certain tolerance.
final int FIRST_EXPECTED_SAMPLE = RECORD2_START_TIME_MS * SAMPLING_RATE_HZ / 1000;
// NOTE: START_TIME_TOLERANCE_MS must always be smaller than RECORD2_START_TIME_MS
final int START_TIME_TOLERANCE_MS = 1;
final int START_SAMPLE_TOLERANCE = START_TIME_TOLERANCE_MS * SAMPLING_RATE_HZ / 1000;
// let time for a resampler to converge by skipping samples at the beginning of the
// record2 buffer before comparing to record1 buffer
final int RESAMPLER_CONVERGENCE_MS = 5;
final int RESAMPLER_CONVERGENCE_SAMPLE =
RESAMPLER_CONVERGENCE_MS * SAMPLING_RATE_HZ / 1000;
boolean buffersMatch = false;
for (int i = -START_SAMPLE_TOLERANCE;
i < START_SAMPLE_TOLERANCE && !buffersMatch; i++) {
int offset1 = i + FIRST_EXPECTED_SAMPLE;
if (offset1 < 0) {
continue;
}
// unlikely: programming error
if (RECORD1_NUM_SAMPLES - offset1 < RECORD2_NUM_SAMPLES) {
Log.w(TAG, "testCapturesMatch: " +
"invalid buffer1 size/buffer2 size/start ms combination!");
break;
}
buffersMatch = true;
for (int j = RESAMPLER_CONVERGENCE_SAMPLE; j < RECORD2_NUM_SAMPLES; j++) {
if (buffer2[j] != buffer1[j + offset1]) {
buffersMatch = false;
break;
}
}
}
assertTrue(buffersMatch);
} finally {
if (record1 != null) {
record1.release();
}
if (record2 != null) {
record2.release();
}
}
}
private boolean hasMicrophone() {
return InstrumentationRegistry.getTargetContext().getPackageManager().hasSystemFeature(
PackageManager.FEATURE_MICROPHONE);
}
private void clearAudioserverPermissionCache() {
try {
SystemUtil.runShellCommand(InstrumentationRegistry.getInstrumentation(),
"cmd media.audio_policy purge_permission-cache");
} catch (IOException e) {
fail("cannot purge audio server permission cache");
}
}
}