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