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