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