Home | History | Annotate | Download | only in apprtc
      1 /*
      2  *  Copyright 2014 The WebRTC Project Authors. All rights reserved.
      3  *
      4  *  Use of this source code is governed by a BSD-style license
      5  *  that can be found in the LICENSE file in the root of the source
      6  *  tree. An additional intellectual property rights grant can be found
      7  *  in the file PATENTS.  All contributing project authors may
      8  *  be found in the AUTHORS file in the root of the source tree.
      9  */
     10 
     11 package org.appspot.apprtc;
     12 
     13 import org.appspot.apprtc.util.AppRTCUtils;
     14 
     15 import android.content.BroadcastReceiver;
     16 import android.content.Context;
     17 import android.content.Intent;
     18 import android.content.IntentFilter;
     19 import android.content.pm.PackageManager;
     20 import android.media.AudioManager;
     21 import android.util.Log;
     22 
     23 import java.util.Collections;
     24 import java.util.HashSet;
     25 import java.util.Set;
     26 
     27 /**
     28  * AppRTCAudioManager manages all audio related parts of the AppRTC demo.
     29  */
     30 public class AppRTCAudioManager {
     31   private static final String TAG = "AppRTCAudioManager";
     32 
     33   /**
     34    * AudioDevice is the names of possible audio devices that we currently
     35    * support.
     36    */
     37   // TODO(henrika): add support for BLUETOOTH as well.
     38   public enum AudioDevice {
     39     SPEAKER_PHONE,
     40     WIRED_HEADSET,
     41     EARPIECE,
     42   }
     43 
     44   private final Context apprtcContext;
     45   private final Runnable onStateChangeListener;
     46   private boolean initialized = false;
     47   private AudioManager audioManager;
     48   private int savedAudioMode = AudioManager.MODE_INVALID;
     49   private boolean savedIsSpeakerPhoneOn = false;
     50   private boolean savedIsMicrophoneMute = false;
     51 
     52   // For now; always use the speaker phone as default device selection when
     53   // there is a choice between SPEAKER_PHONE and EARPIECE.
     54   // TODO(henrika): it is possible that EARPIECE should be preferred in some
     55   // cases. If so, we should set this value at construction instead.
     56   private final AudioDevice defaultAudioDevice = AudioDevice.SPEAKER_PHONE;
     57 
     58   // Proximity sensor object. It measures the proximity of an object in cm
     59   // relative to the view screen of a device and can therefore be used to
     60   // assist device switching (close to ear <=> use headset earpiece if
     61   // available, far from ear <=> use speaker phone).
     62   private AppRTCProximitySensor proximitySensor = null;
     63 
     64   // Contains the currently selected audio device.
     65   private AudioDevice selectedAudioDevice;
     66 
     67   // Contains a list of available audio devices. A Set collection is used to
     68   // avoid duplicate elements.
     69   private final Set<AudioDevice> audioDevices = new HashSet<AudioDevice>();
     70 
     71   // Broadcast receiver for wired headset intent broadcasts.
     72   private BroadcastReceiver wiredHeadsetReceiver;
     73 
     74   // This method is called when the proximity sensor reports a state change,
     75   // e.g. from "NEAR to FAR" or from "FAR to NEAR".
     76   private void onProximitySensorChangedState() {
     77     // The proximity sensor should only be activated when there are exactly two
     78     // available audio devices.
     79     if (audioDevices.size() == 2
     80         && audioDevices.contains(AppRTCAudioManager.AudioDevice.EARPIECE)
     81         && audioDevices.contains(
     82             AppRTCAudioManager.AudioDevice.SPEAKER_PHONE)) {
     83       if (proximitySensor.sensorReportsNearState()) {
     84         // Sensor reports that a "handset is being held up to a person's ear",
     85         // or "something is covering the light sensor".
     86         setAudioDevice(AppRTCAudioManager.AudioDevice.EARPIECE);
     87       } else {
     88         // Sensor reports that a "handset is removed from a person's ear", or
     89         // "the light sensor is no longer covered".
     90         setAudioDevice(AppRTCAudioManager.AudioDevice.SPEAKER_PHONE);
     91       }
     92     }
     93   }
     94 
     95   /** Construction */
     96   static AppRTCAudioManager create(Context context,
     97       Runnable deviceStateChangeListener) {
     98     return new AppRTCAudioManager(context, deviceStateChangeListener);
     99   }
    100 
    101   private AppRTCAudioManager(Context context,
    102       Runnable deviceStateChangeListener) {
    103     apprtcContext = context;
    104     onStateChangeListener = deviceStateChangeListener;
    105     audioManager = ((AudioManager) context.getSystemService(
    106         Context.AUDIO_SERVICE));
    107 
    108     // Create and initialize the proximity sensor.
    109     // Tablet devices (e.g. Nexus 7) does not support proximity sensors.
    110     // Note that, the sensor will not be active until start() has been called.
    111     proximitySensor = AppRTCProximitySensor.create(context, new Runnable() {
    112       // This method will be called each time a state change is detected.
    113       // Example: user holds his hand over the device (closer than ~5 cm),
    114       // or removes his hand from the device.
    115       public void run() {
    116         onProximitySensorChangedState();
    117       }
    118     });
    119     AppRTCUtils.logDeviceInfo(TAG);
    120   }
    121 
    122   public void init() {
    123     Log.d(TAG, "init");
    124     if (initialized) {
    125       return;
    126     }
    127 
    128     // Store current audio state so we can restore it when close() is called.
    129     savedAudioMode = audioManager.getMode();
    130     savedIsSpeakerPhoneOn = audioManager.isSpeakerphoneOn();
    131     savedIsMicrophoneMute = audioManager.isMicrophoneMute();
    132 
    133     // Request audio focus before making any device switch.
    134     audioManager.requestAudioFocus(null, AudioManager.STREAM_VOICE_CALL,
    135         AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
    136 
    137     // Start by setting MODE_IN_COMMUNICATION as default audio mode. It is
    138     // required to be in this mode when playout and/or recording starts for
    139     // best possible VoIP performance.
    140     // TODO(henrika): we migh want to start with RINGTONE mode here instead.
    141     audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
    142 
    143     // Always disable microphone mute during a WebRTC call.
    144     setMicrophoneMute(false);
    145 
    146     // Do initial selection of audio device. This setting can later be changed
    147     // either by adding/removing a wired headset or by covering/uncovering the
    148     // proximity sensor.
    149     updateAudioDeviceState(hasWiredHeadset());
    150 
    151     // Register receiver for broadcast intents related to adding/removing a
    152     // wired headset (Intent.ACTION_HEADSET_PLUG).
    153     registerForWiredHeadsetIntentBroadcast();
    154 
    155     initialized = true;
    156   }
    157 
    158   public void close() {
    159     Log.d(TAG, "close");
    160     if (!initialized) {
    161       return;
    162     }
    163 
    164     unregisterForWiredHeadsetIntentBroadcast();
    165 
    166     // Restore previously stored audio states.
    167     setSpeakerphoneOn(savedIsSpeakerPhoneOn);
    168     setMicrophoneMute(savedIsMicrophoneMute);
    169     audioManager.setMode(savedAudioMode);
    170     audioManager.abandonAudioFocus(null);
    171 
    172     if (proximitySensor != null) {
    173       proximitySensor.stop();
    174       proximitySensor = null;
    175     }
    176 
    177     initialized = false;
    178   }
    179 
    180   /** Changes selection of the currently active audio device. */
    181   public void setAudioDevice(AudioDevice device) {
    182     Log.d(TAG, "setAudioDevice(device=" + device + ")");
    183     AppRTCUtils.assertIsTrue(audioDevices.contains(device));
    184 
    185     switch (device) {
    186       case SPEAKER_PHONE:
    187         setSpeakerphoneOn(true);
    188         selectedAudioDevice = AudioDevice.SPEAKER_PHONE;
    189         break;
    190       case EARPIECE:
    191         setSpeakerphoneOn(false);
    192         selectedAudioDevice = AudioDevice.EARPIECE;
    193         break;
    194       case WIRED_HEADSET:
    195         setSpeakerphoneOn(false);
    196         selectedAudioDevice = AudioDevice.WIRED_HEADSET;
    197         break;
    198       default:
    199         Log.e(TAG, "Invalid audio device selection");
    200         break;
    201     }
    202     onAudioManagerChangedState();
    203   }
    204 
    205   /** Returns current set of available/selectable audio devices. */
    206   public Set<AudioDevice> getAudioDevices() {
    207     return Collections.unmodifiableSet(new HashSet<AudioDevice>(audioDevices));
    208   }
    209 
    210   /** Returns the currently selected audio device. */
    211   public AudioDevice getSelectedAudioDevice() {
    212     return selectedAudioDevice;
    213   }
    214 
    215   /**
    216    * Registers receiver for the broadcasted intent when a wired headset is
    217    * plugged in or unplugged. The received intent will have an extra
    218    * 'state' value where 0 means unplugged, and 1 means plugged.
    219    */
    220   private void registerForWiredHeadsetIntentBroadcast() {
    221     IntentFilter filter = new IntentFilter(Intent.ACTION_HEADSET_PLUG);
    222 
    223     /** Receiver which handles changes in wired headset availability. */
    224     wiredHeadsetReceiver = new BroadcastReceiver() {
    225       private static final int STATE_UNPLUGGED = 0;
    226       private static final int STATE_PLUGGED = 1;
    227       private static final int HAS_NO_MIC = 0;
    228       private static final int HAS_MIC = 1;
    229 
    230       @Override
    231       public void onReceive(Context context, Intent intent) {
    232         int state = intent.getIntExtra("state", STATE_UNPLUGGED);
    233         int microphone = intent.getIntExtra("microphone", HAS_NO_MIC);
    234         String name = intent.getStringExtra("name");
    235         Log.d(TAG, "BroadcastReceiver.onReceive" + AppRTCUtils.getThreadInfo()
    236             + ": "
    237             + "a=" + intent.getAction()
    238             + ", s=" + (state == STATE_UNPLUGGED ? "unplugged" : "plugged")
    239             + ", m=" + (microphone == HAS_MIC ? "mic" : "no mic")
    240             + ", n=" + name
    241             + ", sb=" + isInitialStickyBroadcast());
    242 
    243         boolean hasWiredHeadset = (state == STATE_PLUGGED) ? true : false;
    244         switch (state) {
    245           case STATE_UNPLUGGED:
    246             updateAudioDeviceState(hasWiredHeadset);
    247             break;
    248           case STATE_PLUGGED:
    249             if (selectedAudioDevice != AudioDevice.WIRED_HEADSET) {
    250               updateAudioDeviceState(hasWiredHeadset);
    251             }
    252             break;
    253           default:
    254             Log.e(TAG, "Invalid state");
    255             break;
    256         }
    257       }
    258     };
    259 
    260     apprtcContext.registerReceiver(wiredHeadsetReceiver, filter);
    261   }
    262 
    263   /** Unregister receiver for broadcasted ACTION_HEADSET_PLUG intent. */
    264   private void unregisterForWiredHeadsetIntentBroadcast() {
    265     apprtcContext.unregisterReceiver(wiredHeadsetReceiver);
    266     wiredHeadsetReceiver = null;
    267   }
    268 
    269   /** Sets the speaker phone mode. */
    270   private void setSpeakerphoneOn(boolean on) {
    271     boolean wasOn = audioManager.isSpeakerphoneOn();
    272     if (wasOn == on) {
    273       return;
    274     }
    275     audioManager.setSpeakerphoneOn(on);
    276   }
    277 
    278   /** Sets the microphone mute state. */
    279   private void setMicrophoneMute(boolean on) {
    280     boolean wasMuted = audioManager.isMicrophoneMute();
    281     if (wasMuted == on) {
    282       return;
    283     }
    284     audioManager.setMicrophoneMute(on);
    285   }
    286 
    287   /** Gets the current earpiece state. */
    288   private boolean hasEarpiece() {
    289     return apprtcContext.getPackageManager().hasSystemFeature(
    290         PackageManager.FEATURE_TELEPHONY);
    291   }
    292 
    293   /**
    294    * Checks whether a wired headset is connected or not.
    295    * This is not a valid indication that audio playback is actually over
    296    * the wired headset as audio routing depends on other conditions. We
    297    * only use it as an early indicator (during initialization) of an attached
    298    * wired headset.
    299    */
    300   @Deprecated
    301   private boolean hasWiredHeadset() {
    302     return audioManager.isWiredHeadsetOn();
    303   }
    304 
    305   /** Update list of possible audio devices and make new device selection. */
    306   private void updateAudioDeviceState(boolean hasWiredHeadset) {
    307     // Update the list of available audio devices.
    308     audioDevices.clear();
    309     if (hasWiredHeadset) {
    310       // If a wired headset is connected, then it is the only possible option.
    311       audioDevices.add(AudioDevice.WIRED_HEADSET);
    312     } else {
    313       // No wired headset, hence the audio-device list can contain speaker
    314       // phone (on a tablet), or speaker phone and earpiece (on mobile phone).
    315       audioDevices.add(AudioDevice.SPEAKER_PHONE);
    316       if (hasEarpiece())  {
    317         audioDevices.add(AudioDevice.EARPIECE);
    318       }
    319     }
    320     Log.d(TAG, "audioDevices: " + audioDevices);
    321 
    322     // Switch to correct audio device given the list of available audio devices.
    323     if (hasWiredHeadset) {
    324       setAudioDevice(AudioDevice.WIRED_HEADSET);
    325     } else {
    326       setAudioDevice(defaultAudioDevice);
    327     }
    328   }
    329 
    330   /** Called each time a new audio device has been added or removed. */
    331   private void onAudioManagerChangedState() {
    332     Log.d(TAG, "onAudioManagerChangedState: devices=" + audioDevices
    333         + ", selected=" + selectedAudioDevice);
    334 
    335     // Enable the proximity sensor if there are two available audio devices
    336     // in the list. Given the current implementation, we know that the choice
    337     // will then be between EARPIECE and SPEAKER_PHONE.
    338     if (audioDevices.size() == 2) {
    339       AppRTCUtils.assertIsTrue(audioDevices.contains(AudioDevice.EARPIECE)
    340           && audioDevices.contains(AudioDevice.SPEAKER_PHONE));
    341       // Start the proximity sensor.
    342       proximitySensor.start();
    343     } else if (audioDevices.size() == 1) {
    344       // Stop the proximity sensor since it is no longer needed.
    345       proximitySensor.stop();
    346     } else {
    347       Log.e(TAG, "Invalid device list");
    348     }
    349 
    350     if (onStateChangeListener != null) {
    351       // Run callback to notify a listening client. The client can then
    352       // use public getters to query the new state.
    353       onStateChangeListener.run();
    354     }
    355   }
    356 }
    357