1 /*
2  * Copyright (C) 2016 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.cts;
18 
19 import android.content.pm.PackageManager;
20 import android.media.AudioDeviceInfo;
21 import android.media.AudioFormat;
22 import android.media.AudioManager;
23 import android.media.AudioPlaybackConfiguration;
24 import android.media.AudioRecord;
25 import android.media.AudioRecordingConfiguration;
26 import android.media.MediaRecorder;
27 import android.os.Handler;
28 import android.os.HandlerThread;
29 import android.os.Looper;
30 import android.os.Parcel;
31 import android.util.Log;
32 
33 import com.android.compatibility.common.util.CtsAndroidTestCase;
34 
35 import java.lang.reflect.Method;
36 import java.util.ArrayList;
37 import java.util.concurrent.CountDownLatch;
38 import java.util.concurrent.TimeUnit;
39 import java.util.Iterator;
40 import java.util.List;
41 
42 @NonMediaMainlineTest
43 public class AudioRecordingConfigurationTest extends CtsAndroidTestCase {
44     private static final String TAG = "AudioRecordingConfigurationTest";
45 
46     private static final int TEST_SAMPLE_RATE = 16000;
47     private static final int TEST_AUDIO_SOURCE = MediaRecorder.AudioSource.VOICE_RECOGNITION;
48 
49     private static final int TEST_TIMING_TOLERANCE_MS = 70;
50     private static final long SLEEP_AFTER_STOP_FOR_INACTIVITY_MS = 1000;
51 
52     private AudioRecord mAudioRecord;
53     private Looper mLooper;
54 
55     @Override
setUp()56     protected void setUp() throws Exception {
57         super.setUp();
58         if (!hasMicrophone()) {
59             return;
60         }
61 
62         /*
63          * InstrumentationTestRunner.onStart() calls Looper.prepare(), which creates a looper
64          * for the current thread. However, since we don't actually call loop() in the test,
65          * any messages queued with that looper will never be consumed. Therefore, we must
66          * create the instance in another thread, either without a looper, so the main looper is
67          * used, or with an active looper.
68          */
69         Thread t = new Thread() {
70             @Override
71             public void run() {
72                 Looper.prepare();
73                 mLooper = Looper.myLooper();
74                 synchronized(this) {
75                     mAudioRecord = new AudioRecord.Builder()
76                                      .setAudioSource(TEST_AUDIO_SOURCE)
77                                      .setAudioFormat(new AudioFormat.Builder()
78                                              .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
79                                              .setSampleRate(TEST_SAMPLE_RATE)
80                                              .setChannelMask(AudioFormat.CHANNEL_IN_MONO)
81                                              .build())
82                                      .build();
83                     this.notify();
84                 }
85                 Looper.loop();
86             }
87         };
88         synchronized(t) {
89             t.start(); // will block until we wait
90             t.wait();
91         }
92         assertNotNull(mAudioRecord);
93         assertNotNull(mLooper);
94     }
95 
96     @Override
tearDown()97     protected void tearDown() throws Exception {
98         if (hasMicrophone()) {
99             mAudioRecord.stop();
100             mAudioRecord.release();
101             mLooper.quit();
102             Thread.sleep(SLEEP_AFTER_STOP_FOR_INACTIVITY_MS);
103         }
104         super.tearDown();
105     }
106 
107     // start a recording and verify it is seen as an active recording
testAudioManagerGetActiveRecordConfigurations()108     public void testAudioManagerGetActiveRecordConfigurations() throws Exception {
109         if (!hasMicrophone()) {
110             return;
111         }
112         AudioManager am = new AudioManager(getContext());
113         assertNotNull("Could not create AudioManager", am);
114 
115         List<AudioRecordingConfiguration> configs = am.getActiveRecordingConfigurations();
116         assertNotNull("Invalid null array of record configurations before recording", configs);
117 
118         assertEquals(AudioRecord.STATE_INITIALIZED, mAudioRecord.getState());
119         mAudioRecord.startRecording();
120         assertEquals(AudioRecord.RECORDSTATE_RECORDING, mAudioRecord.getRecordingState());
121         Thread.sleep(TEST_TIMING_TOLERANCE_MS);
122 
123         // recording is active, verify there is an active record configuration
124         configs = am.getActiveRecordingConfigurations();
125         assertNotNull("Invalid null array of record configurations during recording", configs);
126         assertTrue("no active record configurations (empty array) during recording",
127                 configs.size() > 0);
128         final int nbConfigsDuringRecording = configs.size();
129 
130         // verify our recording shows as one of the recording configs
131         assertTrue("Test source/session not amongst active record configurations",
132                 verifyAudioConfig(TEST_AUDIO_SOURCE, mAudioRecord.getAudioSessionId(),
133                         mAudioRecord.getFormat(), mAudioRecord.getRoutedDevice(), configs));
134 
135         // testing public API here: verify no system-privileged info is exposed through reflection
136         verifyPrivilegedInfoIsSafe(configs.get(0));
137 
138         // stopping recording: verify there are less active record configurations
139         mAudioRecord.stop();
140         Thread.sleep(SLEEP_AFTER_STOP_FOR_INACTIVITY_MS);
141         configs = am.getActiveRecordingConfigurations();
142         assertEquals("Unexpected number of recording configs after stop",
143                 configs.size(), 0);
144     }
145 
testCallback()146     public void testCallback() throws Exception {
147         if (!hasMicrophone()) {
148             return;
149         }
150         doCallbackTest(false /* no custom Handler for callback */);
151     }
152 
testCallbackHandler()153     public void testCallbackHandler() throws Exception {
154         if (!hasMicrophone()) {
155             return;
156         }
157         doCallbackTest(true /* use custom Handler for callback */);
158     }
159 
doCallbackTest(boolean useHandlerInCallback)160     private void doCallbackTest(boolean useHandlerInCallback) throws Exception {
161         final Handler h;
162         if (useHandlerInCallback) {
163             HandlerThread handlerThread = new HandlerThread(TAG);
164             handlerThread.start();
165             h = new Handler(handlerThread.getLooper());
166         } else {
167             h = null;
168         }
169         try {
170             AudioManager am = new AudioManager(getContext());
171             assertNotNull("Could not create AudioManager", am);
172 
173             MyAudioRecordingCallback callback = new MyAudioRecordingCallback(
174                     mAudioRecord.getAudioSessionId(), TEST_AUDIO_SOURCE);
175             am.registerAudioRecordingCallback(callback, h /*handler*/);
176 
177             assertEquals(AudioRecord.STATE_INITIALIZED, mAudioRecord.getState());
178             mAudioRecord.startRecording();
179             assertEquals(AudioRecord.RECORDSTATE_RECORDING, mAudioRecord.getRecordingState());
180             callback.await(TEST_TIMING_TOLERANCE_MS);
181 
182             assertTrue("AudioRecordingCallback not called after start", callback.mCalled);
183             Thread.sleep(TEST_TIMING_TOLERANCE_MS);
184 
185             final AudioDeviceInfo testDevice = mAudioRecord.getRoutedDevice();
186             assertTrue("AudioRecord null routed device after start", testDevice != null);
187             final boolean match = verifyAudioConfig(mAudioRecord.getAudioSource(),
188                     mAudioRecord.getAudioSessionId(), mAudioRecord.getFormat(),
189                     testDevice, callback.mConfigs);
190             assertTrue("Expected record configuration was not found", match);
191 
192             // testing public API here: verify no system-privileged info is exposed through
193             // reflection
194             verifyPrivilegedInfoIsSafe(callback.mConfigs.get(0));
195 
196             // stopping recording: callback is called with no match
197             callback.reset();
198             mAudioRecord.stop();
199             callback.await(TEST_TIMING_TOLERANCE_MS);
200             assertTrue("AudioRecordingCallback not called after stop", callback.mCalled);
201             assertEquals("Should not have found record configurations", callback.mConfigs.size(),
202                     0);
203             Thread.sleep(SLEEP_AFTER_STOP_FOR_INACTIVITY_MS);
204 
205             // unregister callback and start recording again
206             am.unregisterAudioRecordingCallback(callback);
207             callback.reset();
208             mAudioRecord.startRecording();
209             callback.await(TEST_TIMING_TOLERANCE_MS);
210             assertFalse("Unregistered callback was called", callback.mCalled);
211             mAudioRecord.stop();
212             Thread.sleep(SLEEP_AFTER_STOP_FOR_INACTIVITY_MS);
213 
214             // just call the callback once directly so it's marked as tested
215             final AudioManager.AudioRecordingCallback arc =
216                     (AudioManager.AudioRecordingCallback) callback;
217             arc.onRecordingConfigChanged(new ArrayList<AudioRecordingConfiguration>());
218         } finally {
219             if (h != null) {
220                 h.getLooper().quit();
221             }
222         }
223     }
224 
225     @NonMediaMainlineTest
testParcel()226     public void testParcel() throws Exception {
227         if (!hasMicrophone()) {
228             return;
229         }
230         AudioManager am = new AudioManager(getContext());
231         assertNotNull("Could not create AudioManager", am);
232 
233         assertEquals(AudioRecord.STATE_INITIALIZED, mAudioRecord.getState());
234         mAudioRecord.startRecording();
235         assertEquals(AudioRecord.RECORDSTATE_RECORDING, mAudioRecord.getRecordingState());
236         Thread.sleep(TEST_TIMING_TOLERANCE_MS);
237 
238         List<AudioRecordingConfiguration> configs = am.getActiveRecordingConfigurations();
239         assertTrue("Empty array of record configs during recording", configs.size() > 0);
240         assertEquals(0, configs.get(0).describeContents());
241 
242         // marshall a AudioRecordingConfiguration and compare to unmarshalled
243         final Parcel srcParcel = Parcel.obtain();
244         final Parcel dstParcel = Parcel.obtain();
245 
246         configs.get(0).writeToParcel(srcParcel, 0 /*no public flags for marshalling*/);
247         final byte[] mbytes = srcParcel.marshall();
248         dstParcel.unmarshall(mbytes, 0, mbytes.length);
249         dstParcel.setDataPosition(0);
250         final AudioRecordingConfiguration unmarshalledConf =
251                 AudioRecordingConfiguration.CREATOR.createFromParcel(dstParcel);
252 
253         assertNotNull("Failure to unmarshall AudioRecordingConfiguration", unmarshalledConf);
254         assertEquals("Source and destination AudioRecordingConfiguration not equal",
255                 configs.get(0), unmarshalledConf);
256     }
257 
258     static class MyAudioRecordingCallback extends AudioManager.AudioRecordingCallback {
259         boolean mCalled;
260         List<AudioRecordingConfiguration> mConfigs;
261         private final int mTestSource;
262         private final int mTestSession;
263         private CountDownLatch mCountDownLatch;
264 
reset()265         void reset() {
266             mCountDownLatch = new CountDownLatch(1);
267             mCalled = false;
268             mConfigs = new ArrayList<AudioRecordingConfiguration>();
269         }
270 
MyAudioRecordingCallback(int session, int source)271         MyAudioRecordingCallback(int session, int source) {
272             mTestSource = source;
273             mTestSession = session;
274             reset();
275         }
276 
277         @Override
onRecordingConfigChanged(List<AudioRecordingConfiguration> configs)278         public void onRecordingConfigChanged(List<AudioRecordingConfiguration> configs) {
279             mCalled = true;
280             mConfigs = configs;
281             mCountDownLatch.countDown();
282         }
283 
await(long timeoutMs)284         void await(long timeoutMs) {
285             try {
286                 mCountDownLatch.await(timeoutMs, TimeUnit.MILLISECONDS);
287             } catch (InterruptedException e) {
288             }
289         }
290     }
291 
deviceMatch(AudioDeviceInfo devJoe, AudioDeviceInfo devJeff)292     private static boolean deviceMatch(AudioDeviceInfo devJoe, AudioDeviceInfo devJeff) {
293         return ((devJoe.getId() == devJeff.getId()
294                 && (devJoe.getAddress() == devJeff.getAddress())
295                 && (devJoe.getType() == devJeff.getType())));
296     }
297 
verifyAudioConfig(int source, int session, AudioFormat format, AudioDeviceInfo device, List<AudioRecordingConfiguration> configs)298     private static boolean verifyAudioConfig(int source, int session, AudioFormat format,
299             AudioDeviceInfo device, List<AudioRecordingConfiguration> configs) {
300         final Iterator<AudioRecordingConfiguration> confIt = configs.iterator();
301         while (confIt.hasNext()) {
302             final AudioRecordingConfiguration config = confIt.next();
303             final AudioDeviceInfo configDevice = config.getAudioDevice();
304             assertTrue("Current recording config has null device", configDevice != null);
305             if ((config.getClientAudioSource() == source)
306                     && (config.getClientAudioSessionId() == session)
307                     // test the client format matches that requested (same as the AudioRecord's)
308                     && (config.getClientFormat().getEncoding() == format.getEncoding())
309                     && (config.getClientFormat().getSampleRate() == format.getSampleRate())
310                     && (config.getClientFormat().getChannelMask() == format.getChannelMask())
311                     && (config.getClientFormat().getChannelIndexMask() ==
312                             format.getChannelIndexMask())
313                     // test the device format is configured
314                     && (config.getFormat().getEncoding() != AudioFormat.ENCODING_INVALID)
315                     && (config.getFormat().getSampleRate() > 0)
316                     //  for the channel mask, either the position or index-based value must be valid
317                     && ((config.getFormat().getChannelMask() != AudioFormat.CHANNEL_INVALID)
318                             || (config.getFormat().getChannelIndexMask() !=
319                                     AudioFormat.CHANNEL_INVALID))
320                     && deviceMatch(device, configDevice)) {
321                 return true;
322             }
323         }
324         return false;
325     }
326 
hasMicrophone()327     private boolean hasMicrophone() {
328         return getContext().getPackageManager().hasSystemFeature(
329                 PackageManager.FEATURE_MICROPHONE);
330     }
331 
verifyPrivilegedInfoIsSafe(AudioRecordingConfiguration config)332     private static void verifyPrivilegedInfoIsSafe(AudioRecordingConfiguration config) {
333         // verify "privileged" fields aren't available through reflection
334         final Class<?> confClass = config.getClass();
335         try {
336             final Method getClientUidMethod = confClass.getDeclaredMethod("getClientUid");
337             final Method getClientPackageName = confClass.getDeclaredMethod("getClientPackageName");
338             Integer uid = (Integer) getClientUidMethod.invoke(config, (Object[]) null);
339             assertEquals("client uid isn't protected", -1 /*expected*/, uid.intValue());
340             String name = (String) getClientPackageName.invoke(config, (Object[]) null);
341             assertNotNull("client package name is null", name);
342             assertEquals("client package name isn't protected", 0 /*expected*/, name.length());
343         } catch (Exception e) {
344             fail("Exception thrown during reflection on config privileged fields" + e);
345         }
346     }
347 }
348