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