1 /*
2  * Copyright (C) 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.media.audio.cts;
18 
19 import static android.media.AudioAttributes.ALLOW_CAPTURE_BY_ALL;
20 
21 import static org.hamcrest.Matchers.greaterThan;
22 import static org.hamcrest.Matchers.is;
23 import static org.junit.Assert.assertEquals;
24 import static org.junit.Assert.assertFalse;
25 import static org.junit.Assert.assertNotNull;
26 import static org.junit.Assert.assertNotSame;
27 import static org.junit.Assert.assertThat;
28 import static org.junit.Assert.assertTrue;
29 import static org.junit.Assert.fail;
30 import static org.junit.Assume.assumeThat;
31 
32 import android.app.Instrumentation;
33 import android.content.Context;
34 import android.media.AudioAttributes;
35 import android.media.AudioAttributes.AttributeUsage;
36 import android.media.AudioAttributes.CapturePolicy;
37 import android.media.AudioFormat;
38 import android.media.AudioManager;
39 import android.media.AudioPlaybackCaptureConfiguration;
40 import android.media.AudioRecord;
41 import android.media.MediaPlayer;
42 import android.media.cts.MediaProjectionActivity;
43 import android.media.cts.Utils;
44 import android.media.projection.MediaProjection;
45 import android.platform.test.annotations.AppModeFull;
46 import android.platform.test.annotations.Presubmit;
47 import android.view.KeyEvent;
48 
49 import androidx.test.platform.app.InstrumentationRegistry;
50 import androidx.test.rule.ActivityTestRule;
51 import androidx.test.uiautomator.UiDevice;
52 
53 import org.junit.After;
54 import org.junit.Before;
55 import org.junit.Rule;
56 import org.junit.Test;
57 
58 import java.nio.ByteBuffer;
59 import java.util.HashMap;
60 import java.util.Map;
61 
62 /**
63  * Validate that there is no discontinuity in the AudioRecord data from Remote Submix.
64  *
65  * The tests do the following:
66  *   - Start AudioRecord and MediaPlayer.
67  *   - Play sine wav audio and read the recorded audio in rawBuffer.
68  *   - Add screen lock during playback.
69  *   - Stop MediaPlayer and AudioRecord, and then unlock the device.
70  *   - Verify that the recorded audio doesn't have any discontinuity.
71  *
72  * Testing at sample level that audio playback and record do not make any alterations to input
73  * signal.
74  */
75 
76 @Presubmit
77 @AppModeFull(reason = "instant apps can't set up test conditions")
78 public class RemoteSubmixTest {
79     private static final String TAG = "RemoteSubmixTest";
80     private static final int SAMPLE_RATE = 44100;
81     private static final int DURATION_IN_SEC = 1;
82     private static final int ENCODING_FORMAT = AudioFormat.ENCODING_PCM_16BIT;
83     private static final int CHANNEL_MASK = AudioFormat.CHANNEL_IN_MONO;
84     private static final int BUFFER_SIZE = SAMPLE_RATE * DURATION_IN_SEC
85             * Integer.bitCount(CHANNEL_MASK)
86             * Short.BYTES; // Size in bytes for 16bit mono at 44.1k/s
87     private static final int TEST_ITERATIONS = 10; // Using iterations for regression failure
88     private static final int RETRY_RECORD_READ = 3;
89 
90     private Context mContext;
91     private AudioManager mAudioManager;
92     private MediaProjectionActivity mActivity;
93     private MediaProjection mMediaProjection;
94     private Map<Integer, Integer> mStreamVolume = new HashMap<Integer, Integer>();
95     private Map<Integer, String> mStreamNames = new HashMap<Integer, String>();
96 
97     @Rule
98     public ActivityTestRule<MediaProjectionActivity> mActivityRule =
99             new ActivityTestRule<>(MediaProjectionActivity.class);
100 
101     @Before
setup()102     public void setup() throws Exception {
103         mActivity = mActivityRule.getActivity();
104         mContext = getInstrumentation().getContext();
105         mAudioManager = mActivity.getSystemService(AudioManager.class);
106         mMediaProjection = mActivity.waitForMediaProjection();
107         mStreamNames.put(AudioManager.STREAM_RING, "RING");
108         mStreamNames.put(AudioManager.STREAM_NOTIFICATION, "NOTIFICATION");
109         mStreamNames.put(AudioManager.STREAM_SYSTEM, "SYSTEM");
110     }
111 
112     @After
tearDown()113     public void tearDown() throws Exception {
114         unmuteStreams();
115     }
116 
getInstrumentation()117     private static Instrumentation getInstrumentation() {
118         return androidx.test.platform.app.InstrumentationRegistry.getInstrumentation();
119     }
120 
createPlaybackCaptureRecord()121     private AudioRecord createPlaybackCaptureRecord() throws Exception {
122         AudioPlaybackCaptureConfiguration apcConfig =
123                 new AudioPlaybackCaptureConfiguration.Builder(mMediaProjection)
124                         .addMatchingUsage(AudioAttributes.USAGE_MEDIA)
125                         .build();
126 
127         AudioFormat audioFormat = new AudioFormat.Builder()
128                                           .setEncoding(ENCODING_FORMAT)
129                                           .setSampleRate(SAMPLE_RATE)
130                                           .setChannelMask(CHANNEL_MASK)
131                                           .build();
132 
133         assertEquals(
134                 "matchingUsages", AudioAttributes.USAGE_MEDIA, apcConfig.getMatchingUsages()[0]);
135 
136         AudioRecord audioRecord = new AudioRecord.Builder()
137                                           .setAudioPlaybackCaptureConfig(apcConfig)
138                                           .setAudioFormat(audioFormat)
139                                           .build();
140 
141         assertEquals("AudioRecord failed to initialized", AudioRecord.STATE_INITIALIZED,
142                 audioRecord.getState());
143 
144         return audioRecord;
145     }
146 
createMediaPlayer( @apturePolicy int capturePolicy, int resid, @AttributeUsage int usage)147     private MediaPlayer createMediaPlayer(
148             @CapturePolicy int capturePolicy, int resid, @AttributeUsage int usage) {
149         MediaPlayer mediaPlayer = MediaPlayer.create(mActivity, resid,
150                 new AudioAttributes.Builder()
151                         .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
152                         .setUsage(usage)
153                         .setAllowedCapturePolicy(capturePolicy)
154                         .build(),
155                 mAudioManager.generateAudioSessionId());
156         return mediaPlayer;
157     }
158 
readToBuffer(AudioRecord audioRecord, int bufferSize)159     private static ByteBuffer readToBuffer(AudioRecord audioRecord, int bufferSize)
160             throws Exception {
161         assertEquals("AudioRecord is not recording", AudioRecord.RECORDSTATE_RECORDING,
162                 audioRecord.getRecordingState());
163         ByteBuffer buffer = ByteBuffer.allocateDirect(bufferSize);
164         int retry = RETRY_RECORD_READ;
165         boolean silence = true;
166         while (silence && buffer.hasRemaining()) {
167             assertNotSame(buffer.remaining() + "/" + bufferSize + " remaining", 0, retry--);
168             int written = audioRecord.read(buffer, buffer.remaining());
169             assertThat("audioRecord did not read frames", written, greaterThan(0));
170             for (int i = 0; i < written; i++) {
171                 if (buffer.get() != 0) {
172                     silence = false;
173                     break;
174                 }
175             }
176         }
177         buffer.rewind();
178         return buffer;
179     }
180 
181     /**
182      * Mute device audio streams
183      */
muteStreams()184     private void muteStreams() throws Exception {
185         try {
186             Utils.toggleNotificationPolicyAccess(
187                     mContext.getPackageName(), getInstrumentation(), true);
188             for (Map.Entry<Integer, String> map : mStreamNames.entrySet()) {
189                 // Get current device stream volume level
190                 mStreamVolume.put(map.getKey(), mAudioManager.getStreamVolume(map.getKey()));
191                 // Mute device streams
192                 mAudioManager.adjustStreamVolume(
193                         map.getKey(), AudioManager.ADJUST_MUTE, 0 /*no flag used*/);
194                 assumeThat("Stream " + map.getValue() + " can not be muted",
195                         mAudioManager.getStreamVolume(map.getKey()), is(0));
196             }
197         } finally {
198             Utils.toggleNotificationPolicyAccess(
199                     mContext.getPackageName(), getInstrumentation(), false);
200         }
201     }
202 
203     /**
204      * Unmute device audio streams
205      */
unmuteStreams()206     private void unmuteStreams() throws Exception {
207         try {
208             Utils.toggleNotificationPolicyAccess(
209                     mContext.getPackageName(), getInstrumentation(), true);
210             for (Map.Entry<Integer, Integer> map : mStreamVolume.entrySet()) {
211                 // Restore device stream volume
212                 mAudioManager.setStreamVolume(map.getKey(), map.getValue(), 0 /*no flag used*/);
213             }
214         } finally {
215             Utils.toggleNotificationPolicyAccess(
216                     mContext.getPackageName(), getInstrumentation(), false);
217         }
218         mStreamVolume.clear();
219     }
220 
testPlaybackCapture(boolean testWithScreenLock)221     public void testPlaybackCapture(boolean testWithScreenLock) throws Exception {
222         MediaPlayer mediaPlayer = createMediaPlayer(
223                 ALLOW_CAPTURE_BY_ALL, R.raw.sine1320hz5sec, AudioAttributes.USAGE_MEDIA);
224         AudioRecord audioRecord = createPlaybackCaptureRecord();
225         ByteBuffer rawBuffer = null;
226 
227         try {
228             audioRecord.startRecording();
229             mediaPlayer.start();
230 
231             assertEquals(AudioRecord.RECORDSTATE_RECORDING, audioRecord.getRecordingState());
232             assertTrue(mediaPlayer.isPlaying());
233 
234             if (testWithScreenLock) {
235                 UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
236                         .pressKeyCode(KeyEvent.KEYCODE_POWER);
237             }
238 
239             rawBuffer = readToBuffer(audioRecord, BUFFER_SIZE);
240 
241             audioRecord.stop();
242             mediaPlayer.stop();
243 
244             assertEquals(AudioRecord.RECORDSTATE_STOPPED, audioRecord.getRecordingState());
245             assertFalse(mediaPlayer.isPlaying());
246 
247         } catch (Exception e) {
248             throw e;
249         } finally {
250             if (testWithScreenLock) {
251                 UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
252                         .pressKeyCode(KeyEvent.KEYCODE_WAKEUP);
253                 UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
254                         .executeShellCommand("wm dismiss-keyguard");
255             }
256 
257             audioRecord.release();
258             mediaPlayer.release();
259         }
260 
261         assertNotNull("Recorded data is null ", rawBuffer);
262 
263         short[] recordArray = new short[BUFFER_SIZE / Short.BYTES];
264 
265         for (int i = 0; i < recordArray.length; i++) {
266             recordArray[i] = rawBuffer.getShort();
267         }
268 
269         int recordingStartIndex = -1;
270 
271         // Skip leading silence of the Recorded Audio
272         for (int i = 0; i < recordArray.length; i++) {
273             if (recordArray[i] != 0) {
274                 recordingStartIndex = i;
275                 break;
276             }
277         }
278 
279         assertFalse("No audio recorded", recordingStartIndex == -1);
280         // Validate that there is no continuous silence in recorded sine audio
281         for (int i = recordingStartIndex; i < recordArray.length; i++) {
282             assertFalse("Discontunuity found in the Record Audio\n",
283                     recordArray[i] == 0 && recordArray[i + 1] == 0);
284         }
285     }
286 
287     @Test
testRemoteSubmixRecordingContinuity()288     public void testRemoteSubmixRecordingContinuity() throws Exception {
289         muteStreams();
290         for (int i = 0; i < TEST_ITERATIONS; i++) {
291             try {
292                 testPlaybackCapture(/* testWithScreenLock */ false);
293             } catch (Exception e) {
294                 fail("testPlaybackCapture throws exception: " + e + " at the " + i
295                         + "th iteration");
296             }
297         }
298     }
299 
300     @Test
testRemoteSubmixRecordingContinuityWithScreenLock()301     public void testRemoteSubmixRecordingContinuityWithScreenLock() throws Exception {
302         muteStreams();
303         for (int i = 0; i < TEST_ITERATIONS; i++) {
304             try {
305                 testPlaybackCapture(/* testWithScreenLock */ true);
306             } catch (Exception e) {
307                 fail("testPlaybackCapture with screen lock throws exception: " + e + " at the " + i
308                         + "th iteration");
309             }
310         }
311     }
312 }
313