Home | History | Annotate | Download | only in cts
      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