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 |