| /* |
| * Copyright 2014 The WebRTC Project Authors. All rights reserved. |
| * |
| * Use of this source code is governed by a BSD-style license |
| * that can be found in the LICENSE file in the root of the source |
| * tree. An additional intellectual property rights grant can be found |
| * in the file PATENTS. All contributing project authors may |
| * be found in the AUTHORS file in the root of the source tree. |
| */ |
| |
| package org.appspot.apprtc; |
| |
| import org.appspot.apprtc.util.AppRTCUtils; |
| |
| import android.content.BroadcastReceiver; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| import android.content.SharedPreferences; |
| import android.content.pm.PackageManager; |
| import android.media.AudioManager; |
| import android.preference.PreferenceManager; |
| import android.util.Log; |
| |
| import java.util.Collections; |
| import java.util.HashSet; |
| import java.util.Set; |
| |
| /** |
| * AppRTCAudioManager manages all audio related parts of the AppRTC demo. |
| */ |
| public class AppRTCAudioManager { |
| private static final String TAG = "AppRTCAudioManager"; |
| private static final String SPEAKERPHONE_AUTO = "auto"; |
| private static final String SPEAKERPHONE_TRUE = "true"; |
| private static final String SPEAKERPHONE_FALSE = "false"; |
| |
| /** |
| * AudioDevice is the names of possible audio devices that we currently |
| * support. |
| */ |
| // TODO(henrika): add support for BLUETOOTH as well. |
| public enum AudioDevice { |
| SPEAKER_PHONE, |
| WIRED_HEADSET, |
| EARPIECE, |
| } |
| |
| private final Context apprtcContext; |
| private final Runnable onStateChangeListener; |
| private boolean initialized = false; |
| private AudioManager audioManager; |
| private int savedAudioMode = AudioManager.MODE_INVALID; |
| private boolean savedIsSpeakerPhoneOn = false; |
| private boolean savedIsMicrophoneMute = false; |
| |
| private final AudioDevice defaultAudioDevice; |
| |
| // Contains speakerphone setting: auto, true or false |
| private final String useSpeakerphone; |
| |
| // Proximity sensor object. It measures the proximity of an object in cm |
| // relative to the view screen of a device and can therefore be used to |
| // assist device switching (close to ear <=> use headset earpiece if |
| // available, far from ear <=> use speaker phone). |
| private AppRTCProximitySensor proximitySensor = null; |
| |
| // Contains the currently selected audio device. |
| private AudioDevice selectedAudioDevice; |
| |
| // Contains a list of available audio devices. A Set collection is used to |
| // avoid duplicate elements. |
| private final Set<AudioDevice> audioDevices = new HashSet<AudioDevice>(); |
| |
| // Broadcast receiver for wired headset intent broadcasts. |
| private BroadcastReceiver wiredHeadsetReceiver; |
| |
| // This method is called when the proximity sensor reports a state change, |
| // e.g. from "NEAR to FAR" or from "FAR to NEAR". |
| private void onProximitySensorChangedState() { |
| if (!useSpeakerphone.equals(SPEAKERPHONE_AUTO)) { |
| return; |
| } |
| |
| // The proximity sensor should only be activated when there are exactly two |
| // available audio devices. |
| if (audioDevices.size() == 2 |
| && audioDevices.contains(AppRTCAudioManager.AudioDevice.EARPIECE) |
| && audioDevices.contains( |
| AppRTCAudioManager.AudioDevice.SPEAKER_PHONE)) { |
| if (proximitySensor.sensorReportsNearState()) { |
| // Sensor reports that a "handset is being held up to a person's ear", |
| // or "something is covering the light sensor". |
| setAudioDevice(AppRTCAudioManager.AudioDevice.EARPIECE); |
| } else { |
| // Sensor reports that a "handset is removed from a person's ear", or |
| // "the light sensor is no longer covered". |
| setAudioDevice(AppRTCAudioManager.AudioDevice.SPEAKER_PHONE); |
| } |
| } |
| } |
| |
| /** Construction */ |
| static AppRTCAudioManager create(Context context, |
| Runnable deviceStateChangeListener) { |
| return new AppRTCAudioManager(context, deviceStateChangeListener); |
| } |
| |
| private AppRTCAudioManager(Context context, |
| Runnable deviceStateChangeListener) { |
| apprtcContext = context; |
| onStateChangeListener = deviceStateChangeListener; |
| audioManager = ((AudioManager) context.getSystemService( |
| Context.AUDIO_SERVICE)); |
| |
| SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); |
| useSpeakerphone = sharedPreferences.getString(context.getString(R.string.pref_speakerphone_key), |
| context.getString(R.string.pref_speakerphone_default)); |
| |
| if (useSpeakerphone.equals(SPEAKERPHONE_FALSE)) { |
| defaultAudioDevice = AudioDevice.EARPIECE; |
| } else { |
| defaultAudioDevice = AudioDevice.SPEAKER_PHONE; |
| } |
| |
| // Create and initialize the proximity sensor. |
| // Tablet devices (e.g. Nexus 7) does not support proximity sensors. |
| // Note that, the sensor will not be active until start() has been called. |
| proximitySensor = AppRTCProximitySensor.create(context, new Runnable() { |
| // This method will be called each time a state change is detected. |
| // Example: user holds his hand over the device (closer than ~5 cm), |
| // or removes his hand from the device. |
| public void run() { |
| onProximitySensorChangedState(); |
| } |
| }); |
| AppRTCUtils.logDeviceInfo(TAG); |
| } |
| |
| public void init() { |
| Log.d(TAG, "init"); |
| if (initialized) { |
| return; |
| } |
| |
| // Store current audio state so we can restore it when close() is called. |
| savedAudioMode = audioManager.getMode(); |
| savedIsSpeakerPhoneOn = audioManager.isSpeakerphoneOn(); |
| savedIsMicrophoneMute = audioManager.isMicrophoneMute(); |
| |
| // Request audio focus before making any device switch. |
| audioManager.requestAudioFocus(null, AudioManager.STREAM_VOICE_CALL, |
| AudioManager.AUDIOFOCUS_GAIN_TRANSIENT); |
| |
| // Start by setting MODE_IN_COMMUNICATION as default audio mode. It is |
| // required to be in this mode when playout and/or recording starts for |
| // best possible VoIP performance. |
| // TODO(henrika): we migh want to start with RINGTONE mode here instead. |
| audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION); |
| |
| // Always disable microphone mute during a WebRTC call. |
| setMicrophoneMute(false); |
| |
| // Do initial selection of audio device. This setting can later be changed |
| // either by adding/removing a wired headset or by covering/uncovering the |
| // proximity sensor. |
| updateAudioDeviceState(hasWiredHeadset()); |
| |
| // Register receiver for broadcast intents related to adding/removing a |
| // wired headset (Intent.ACTION_HEADSET_PLUG). |
| registerForWiredHeadsetIntentBroadcast(); |
| |
| initialized = true; |
| } |
| |
| public void close() { |
| Log.d(TAG, "close"); |
| if (!initialized) { |
| return; |
| } |
| |
| unregisterForWiredHeadsetIntentBroadcast(); |
| |
| // Restore previously stored audio states. |
| setSpeakerphoneOn(savedIsSpeakerPhoneOn); |
| setMicrophoneMute(savedIsMicrophoneMute); |
| audioManager.setMode(savedAudioMode); |
| audioManager.abandonAudioFocus(null); |
| |
| if (proximitySensor != null) { |
| proximitySensor.stop(); |
| proximitySensor = null; |
| } |
| |
| initialized = false; |
| } |
| |
| /** Changes selection of the currently active audio device. */ |
| public void setAudioDevice(AudioDevice device) { |
| Log.d(TAG, "setAudioDevice(device=" + device + ")"); |
| AppRTCUtils.assertIsTrue(audioDevices.contains(device)); |
| |
| switch (device) { |
| case SPEAKER_PHONE: |
| setSpeakerphoneOn(true); |
| selectedAudioDevice = AudioDevice.SPEAKER_PHONE; |
| break; |
| case EARPIECE: |
| setSpeakerphoneOn(false); |
| selectedAudioDevice = AudioDevice.EARPIECE; |
| break; |
| case WIRED_HEADSET: |
| setSpeakerphoneOn(false); |
| selectedAudioDevice = AudioDevice.WIRED_HEADSET; |
| break; |
| default: |
| Log.e(TAG, "Invalid audio device selection"); |
| break; |
| } |
| onAudioManagerChangedState(); |
| } |
| |
| /** Returns current set of available/selectable audio devices. */ |
| public Set<AudioDevice> getAudioDevices() { |
| return Collections.unmodifiableSet(new HashSet<AudioDevice>(audioDevices)); |
| } |
| |
| /** Returns the currently selected audio device. */ |
| public AudioDevice getSelectedAudioDevice() { |
| return selectedAudioDevice; |
| } |
| |
| /** |
| * Registers receiver for the broadcasted intent when a wired headset is |
| * plugged in or unplugged. The received intent will have an extra |
| * 'state' value where 0 means unplugged, and 1 means plugged. |
| */ |
| private void registerForWiredHeadsetIntentBroadcast() { |
| IntentFilter filter = new IntentFilter(Intent.ACTION_HEADSET_PLUG); |
| |
| /** Receiver which handles changes in wired headset availability. */ |
| wiredHeadsetReceiver = new BroadcastReceiver() { |
| private static final int STATE_UNPLUGGED = 0; |
| private static final int STATE_PLUGGED = 1; |
| private static final int HAS_NO_MIC = 0; |
| private static final int HAS_MIC = 1; |
| |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| int state = intent.getIntExtra("state", STATE_UNPLUGGED); |
| int microphone = intent.getIntExtra("microphone", HAS_NO_MIC); |
| String name = intent.getStringExtra("name"); |
| Log.d(TAG, "BroadcastReceiver.onReceive" + AppRTCUtils.getThreadInfo() |
| + ": " |
| + "a=" + intent.getAction() |
| + ", s=" + (state == STATE_UNPLUGGED ? "unplugged" : "plugged") |
| + ", m=" + (microphone == HAS_MIC ? "mic" : "no mic") |
| + ", n=" + name |
| + ", sb=" + isInitialStickyBroadcast()); |
| |
| boolean hasWiredHeadset = (state == STATE_PLUGGED); |
| switch (state) { |
| case STATE_UNPLUGGED: |
| updateAudioDeviceState(hasWiredHeadset); |
| break; |
| case STATE_PLUGGED: |
| if (selectedAudioDevice != AudioDevice.WIRED_HEADSET) { |
| updateAudioDeviceState(hasWiredHeadset); |
| } |
| break; |
| default: |
| Log.e(TAG, "Invalid state"); |
| break; |
| } |
| } |
| }; |
| |
| apprtcContext.registerReceiver(wiredHeadsetReceiver, filter); |
| } |
| |
| /** Unregister receiver for broadcasted ACTION_HEADSET_PLUG intent. */ |
| private void unregisterForWiredHeadsetIntentBroadcast() { |
| apprtcContext.unregisterReceiver(wiredHeadsetReceiver); |
| wiredHeadsetReceiver = null; |
| } |
| |
| /** Sets the speaker phone mode. */ |
| private void setSpeakerphoneOn(boolean on) { |
| boolean wasOn = audioManager.isSpeakerphoneOn(); |
| if (wasOn == on) { |
| return; |
| } |
| audioManager.setSpeakerphoneOn(on); |
| } |
| |
| /** Sets the microphone mute state. */ |
| private void setMicrophoneMute(boolean on) { |
| boolean wasMuted = audioManager.isMicrophoneMute(); |
| if (wasMuted == on) { |
| return; |
| } |
| audioManager.setMicrophoneMute(on); |
| } |
| |
| /** Gets the current earpiece state. */ |
| private boolean hasEarpiece() { |
| return apprtcContext.getPackageManager().hasSystemFeature( |
| PackageManager.FEATURE_TELEPHONY); |
| } |
| |
| /** |
| * Checks whether a wired headset is connected or not. |
| * This is not a valid indication that audio playback is actually over |
| * the wired headset as audio routing depends on other conditions. We |
| * only use it as an early indicator (during initialization) of an attached |
| * wired headset. |
| */ |
| @Deprecated |
| private boolean hasWiredHeadset() { |
| return audioManager.isWiredHeadsetOn(); |
| } |
| |
| /** Update list of possible audio devices and make new device selection. */ |
| private void updateAudioDeviceState(boolean hasWiredHeadset) { |
| // Update the list of available audio devices. |
| audioDevices.clear(); |
| if (hasWiredHeadset) { |
| // If a wired headset is connected, then it is the only possible option. |
| audioDevices.add(AudioDevice.WIRED_HEADSET); |
| } else { |
| // No wired headset, hence the audio-device list can contain speaker |
| // phone (on a tablet), or speaker phone and earpiece (on mobile phone). |
| audioDevices.add(AudioDevice.SPEAKER_PHONE); |
| if (hasEarpiece()) { |
| audioDevices.add(AudioDevice.EARPIECE); |
| } |
| } |
| Log.d(TAG, "audioDevices: " + audioDevices); |
| |
| // Switch to correct audio device given the list of available audio devices. |
| if (hasWiredHeadset) { |
| setAudioDevice(AudioDevice.WIRED_HEADSET); |
| } else { |
| setAudioDevice(defaultAudioDevice); |
| } |
| } |
| |
| /** Called each time a new audio device has been added or removed. */ |
| private void onAudioManagerChangedState() { |
| Log.d(TAG, "onAudioManagerChangedState: devices=" + audioDevices |
| + ", selected=" + selectedAudioDevice); |
| |
| // Enable the proximity sensor if there are two available audio devices |
| // in the list. Given the current implementation, we know that the choice |
| // will then be between EARPIECE and SPEAKER_PHONE. |
| if (audioDevices.size() == 2) { |
| AppRTCUtils.assertIsTrue(audioDevices.contains(AudioDevice.EARPIECE) |
| && audioDevices.contains(AudioDevice.SPEAKER_PHONE)); |
| // Start the proximity sensor. |
| proximitySensor.start(); |
| } else if (audioDevices.size() == 1) { |
| // Stop the proximity sensor since it is no longer needed. |
| proximitySensor.stop(); |
| } else { |
| Log.e(TAG, "Invalid device list"); |
| } |
| |
| if (onStateChangeListener != null) { |
| // Run callback to notify a listening client. The client can then |
| // use public getters to query the new state. |
| onStateChangeListener.run(); |
| } |
| } |
| } |