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