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.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