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