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