Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 /* | 1 /* |
| 2 * Copyright 2014 The WebRTC Project Authors. All rights reserved. | 2 * Copyright 2014 The WebRTC Project Authors. All rights reserved. |
| 3 * | 3 * |
| 4 * Use of this source code is governed by a BSD-style license | 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 | 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 | 6 * tree. An additional intellectual property rights grant can be found |
| 7 * in the file PATENTS. All contributing project authors may | 7 * in the file PATENTS. All contributing project authors may |
| 8 * be found in the AUTHORS file in the root of the source tree. | 8 * be found in the AUTHORS file in the root of the source tree. |
| 9 */ | 9 */ |
| 10 | 10 |
| 11 package org.appspot.apprtc; | 11 package org.appspot.apprtc; |
| 12 | 12 |
| 13 import org.appspot.apprtc.util.AppRTCUtils; | 13 import org.appspot.apprtc.util.AppRTCUtils; |
| 14 | 14 |
| 15 import android.bluetooth.BluetoothAdapter; | |
| 16 import android.bluetooth.BluetoothDevice; | |
| 17 import android.bluetooth.BluetoothHeadset; | |
| 18 import android.bluetooth.BluetoothProfile; | |
| 15 import android.content.BroadcastReceiver; | 19 import android.content.BroadcastReceiver; |
| 16 import android.content.Context; | 20 import android.content.Context; |
| 17 import android.content.Intent; | 21 import android.content.Intent; |
| 18 import android.content.IntentFilter; | 22 import android.content.IntentFilter; |
| 19 import android.content.SharedPreferences; | 23 import android.content.SharedPreferences; |
| 20 import android.content.pm.PackageManager; | 24 import android.content.pm.PackageManager; |
| 21 import android.media.AudioManager; | 25 import android.media.AudioManager; |
| 26 import android.os.Handler; | |
| 27 import android.os.HandlerThread; | |
| 28 import android.os.Process; | |
| 22 import android.preference.PreferenceManager; | 29 import android.preference.PreferenceManager; |
| 23 import android.util.Log; | 30 import android.util.Log; |
| 24 | 31 |
| 25 import java.util.Collections; | 32 import java.util.Collections; |
| 26 import java.util.HashSet; | 33 import java.util.HashSet; |
| 34 import java.util.List; | |
| 27 import java.util.Set; | 35 import java.util.Set; |
| 28 | 36 |
| 29 /** | 37 // AppRTCAudioManager manages all audio related parts of the AppRTC demo. |
|
magjed_webrtc
2016/11/28 12:59:45
We are not following the style guide strictly when
henrika_webrtc
2016/11/28 14:06:18
Acknowledged.
| |
| 30 * AppRTCAudioManager manages all audio related parts of the AppRTC demo. | |
| 31 */ | |
| 32 public class AppRTCAudioManager { | 38 public class AppRTCAudioManager { |
| 33 private static final String TAG = "AppRTCAudioManager"; | 39 private static final String TAG = "AppRTCAudioManager"; |
| 40 private static final String TAG_BT = "AppRTCAudioManagerBT"; | |
| 34 private static final String SPEAKERPHONE_AUTO = "auto"; | 41 private static final String SPEAKERPHONE_AUTO = "auto"; |
| 35 private static final String SPEAKERPHONE_TRUE = "true"; | 42 private static final String SPEAKERPHONE_TRUE = "true"; |
| 36 private static final String SPEAKERPHONE_FALSE = "false"; | 43 private static final String SPEAKERPHONE_FALSE = "false"; |
| 37 | 44 |
| 38 /** | 45 // Timeout interval for starting or stopping audio to a Bluetooth SCO device. |
| 39 * AudioDevice is the names of possible audio devices that we currently | 46 private static final int BLUETOOTH_SCO_TIMEOUT_MS = 4000; |
| 40 * support. | 47 // Maximum number of SCO connection attempts. |
| 41 */ | 48 private static final int MAX_SCO_CONNECTION_ATTEMPTS = 2; |
| 42 // TODO(henrika): add support for BLUETOOTH as well. | 49 |
| 43 public enum AudioDevice { | 50 // AudioDevice is the names of possible audio devices that we currently |
| 44 SPEAKER_PHONE, | 51 // support. |
| 45 WIRED_HEADSET, | 52 public enum AudioDevice { SPEAKER_PHONE, WIRED_HEADSET, EARPIECE, BLUETOOTH, N ONE } |
| 46 EARPIECE, | 53 |
| 54 // AudioManager state. | |
| 55 public enum AudioManagerState { | |
| 56 UNINITIALIZED, | |
| 57 PREINITIALIZED, | |
| 58 RUNNING, | |
| 59 } | |
| 60 | |
| 61 // Bluetooth connection state. | |
| 62 public enum BluetoothState { | |
| 63 // Bluetooth is not available; no adapter or Bluetooth is off. | |
| 64 UNINITIALIZED, | |
| 65 // Bluetooth error happened when trying to start Bluetooth. | |
| 66 ERROR, | |
| 67 // Bluetooth SCO proxy is connected, but no connected Bluetooth headset devi ces, | |
| 68 // SCO is not started or disconnected. | |
| 69 HEADSET_UNAVAILABLE, | |
| 70 // Bluetooth SCO proxy is connected, connected Bluetooth headset present, bu t SCO is | |
| 71 // not started or disconnected. | |
| 72 HEADSET_AVAILABLE, | |
| 73 // Bluetooth audio SCO connection with remote device is closing. | |
| 74 SCO_DISCONNECTING, | |
| 75 // Bluetooth audio SCO connection with remote device is initiated. | |
| 76 SCO_CONNECTING, | |
| 77 // Bluetooth audio SCO connection with remote device is established. | |
| 78 SCO_CONNECTED | |
| 79 } | |
| 80 | |
| 81 // Selected audio device change event. | |
| 82 public static interface AudioManagerEvents { | |
| 83 // Callback fired once audio device is changed or list of available audio de vices changed. | |
| 84 void onAudioDeviceChanged( | |
| 85 AudioDevice selectedAudioDevice, Set<AudioDevice> availableAudioDevices) ; | |
| 47 } | 86 } |
| 48 | 87 |
| 49 private final Context apprtcContext; | 88 private final Context apprtcContext; |
| 50 private final Runnable onStateChangeListener; | |
| 51 private boolean initialized = false; | |
| 52 private AudioManager audioManager; | 89 private AudioManager audioManager; |
| 90 private final Handler handler; | |
| 91 | |
| 92 private AudioManagerEvents audioManagerEvents; | |
| 93 private AudioManagerState amState; | |
| 53 private int savedAudioMode = AudioManager.MODE_INVALID; | 94 private int savedAudioMode = AudioManager.MODE_INVALID; |
| 54 private boolean savedIsSpeakerPhoneOn = false; | 95 private boolean savedIsSpeakerPhoneOn = false; |
| 55 private boolean savedIsMicrophoneMute = false; | 96 private boolean savedIsMicrophoneMute = false; |
| 97 private boolean hasWiredHeadset = false; | |
| 56 | 98 |
| 57 private final AudioDevice defaultAudioDevice; | 99 // Default audio device; speaker phone for video calls or earpiece for audio |
| 100 // only calls. | |
| 101 private AudioDevice defaultAudioDevice; | |
| 102 | |
| 103 // Contains the currently selected audio device. | |
| 104 private AudioDevice selectedAudioDevice; | |
| 105 | |
| 106 // Contains user selected audio device. | |
| 107 // TODO(henrika): always set to AudioDevice.NONE today. Add support for | |
| 108 // explicit selection based on choice by userSelectedAudioDevice. | |
| 109 private AudioDevice userSelectedAudioDevice; | |
| 58 | 110 |
| 59 // Contains speakerphone setting: auto, true or false | 111 // Contains speakerphone setting: auto, true or false |
| 60 private final String useSpeakerphone; | 112 private final String useSpeakerphone; |
| 61 | 113 |
| 62 // Proximity sensor object. It measures the proximity of an object in cm | 114 // Enabled during initialization if BLUETOOTH permission is granted. |
| 63 // relative to the view screen of a device and can therefore be used to | 115 private boolean hasBluetoothPermission = false; |
|
magjed_webrtc
2016/11/28 12:59:45
I don't think it's a good idea to cache permission
henrika_webrtc
2016/11/28 14:06:18
Acknowledged.
| |
| 64 // assist device switching (close to ear <=> use headset earpiece if | |
| 65 // available, far from ear <=> use speaker phone). | |
| 66 private AppRTCProximitySensor proximitySensor = null; | |
| 67 | 116 |
| 68 // Contains the currently selected audio device. | 117 // Listener for when we have been connected or disconnected to the Headset |
| 69 private AudioDevice selectedAudioDevice; | 118 // Bluetooth profile. |
| 119 private final BluetoothProfile.ServiceListener bluetoothServiceListener; | |
| 120 | |
| 121 private BluetoothState bluetoothState; | |
| 122 private BluetoothAdapter bluetoothAdapter; | |
| 123 private BluetoothHeadset bluetoothHeadset; | |
| 124 private BluetoothDevice bluetoothDevice; | |
| 125 // Number of Bluetooth SCO connection attempts. | |
| 126 int scoConnectionAttempts; | |
| 70 | 127 |
| 71 // Contains a list of available audio devices. A Set collection is used to | 128 // Contains a list of available audio devices. A Set collection is used to |
| 72 // avoid duplicate elements. | 129 // avoid duplicate elements. |
| 73 private final Set<AudioDevice> audioDevices = new HashSet<AudioDevice>(); | 130 private final Set<AudioDevice> audioDevices = new HashSet<AudioDevice>(); |
| 74 | 131 |
| 75 // Broadcast receiver for wired headset intent broadcasts. | 132 // Broadcast receiver for wired headset intent broadcasts. |
| 76 private BroadcastReceiver wiredHeadsetReceiver; | 133 private BroadcastReceiver wiredHeadsetReceiver; |
| 77 | 134 |
| 135 // Broadcast receiver for Bluetooth headset intent broadcasts. Detects | |
| 136 // headset changes and Bluetooth SCO state changes. | |
| 137 private final BroadcastReceiver bluetoothHeadsetReceiver; | |
| 138 | |
| 78 // Callback method for changes in audio focus. | 139 // Callback method for changes in audio focus. |
| 79 private AudioManager.OnAudioFocusChangeListener audioFocusChangeListener; | 140 private AudioManager.OnAudioFocusChangeListener audioFocusChangeListener; |
| 80 | 141 |
| 81 // This method is called when the proximity sensor reports a state change, | 142 // Runs when the Bluetooth timeout expires. We use that timeout after calling |
| 82 // e.g. from "NEAR to FAR" or from "FAR to NEAR". | 143 // startBluetoothSco() or stopBluetoothSco() because we're not guaranteed to |
| 83 private void onProximitySensorChangedState() { | 144 // get a callback after those calls. |
| 84 if (!useSpeakerphone.equals(SPEAKERPHONE_AUTO)) { | 145 private final Runnable bluetoothTimeoutRunnable = new Runnable() { |
| 85 return; | 146 @Override |
| 147 public void run() { | |
| 148 bluetoothTimeout(); | |
| 149 } | |
| 150 }; | |
| 151 | |
| 152 // Receiver which handles changes in wired headset availability. | |
| 153 private class WiredHeadsetReceiver extends BroadcastReceiver { | |
| 154 private static final int STATE_UNPLUGGED = 0; | |
| 155 private static final int STATE_PLUGGED = 1; | |
| 156 private static final int HAS_NO_MIC = 0; | |
| 157 private static final int HAS_MIC = 1; | |
| 158 | |
| 159 @Override | |
| 160 public void onReceive(Context context, Intent intent) { | |
| 161 int state = intent.getIntExtra("state", STATE_UNPLUGGED); | |
| 162 int microphone = intent.getIntExtra("microphone", HAS_NO_MIC); | |
| 163 String name = intent.getStringExtra("name"); | |
| 164 Log.d(TAG, "WiredHeadsetReceiver.onReceive" + AppRTCUtils.getThreadInfo() + ": " | |
| 165 + "a=" + intent.getAction() + ", s=" | |
| 166 + (state == STATE_UNPLUGGED ? "unplugged" : "plugged") + ", m=" | |
| 167 + (microphone == HAS_MIC ? "mic" : "no mic") + ", n=" + name + ", sb=" | |
| 168 + isInitialStickyBroadcast()); | |
| 169 hasWiredHeadset = (state == STATE_PLUGGED); | |
| 170 updateAudioDeviceState(); | |
| 171 } | |
| 172 }; | |
| 173 | |
| 174 // Intent broadcast receiver which handles changes in Bluetooth device availab ility. | |
| 175 private class BluetoothHeadsetBroadcastReceiver extends BroadcastReceiver { | |
| 176 @Override | |
| 177 public void onReceive(Context context, Intent intent) { | |
| 178 if (amState != AudioManagerState.RUNNING || bluetoothState == BluetoothSta te.UNINITIALIZED) { | |
| 179 return; | |
| 180 } | |
| 181 final String action = intent.getAction(); | |
| 182 // Change in connection state of the Headset profile. Note that the | |
| 183 // change does not tell us anything about whether we're streaming | |
| 184 // audio to BT over SCO. | |
| 185 if (action.equals(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED)) { | |
| 186 final int state = | |
| 187 intent.getIntExtra(BluetoothHeadset.EXTRA_STATE, BluetoothHeadset.ST ATE_DISCONNECTED); | |
| 188 final BluetoothDevice device = | |
| 189 (BluetoothDevice) intent.getParcelableExtra(BluetoothDevice.EXTRA_DE VICE); | |
| 190 Log.d(TAG_BT, "BluetoothHeadsetBroadcastReceiver.onReceive: " | |
| 191 + "a=ACTION_CONNECTION_STATE_CHANGED, " | |
| 192 + "s=" + stateToString(state) + ", " | |
| 193 + "n=" + device.getName() + ", " | |
| 194 + "sb=" + isInitialStickyBroadcast() + ", " | |
| 195 + "BT state: " + bluetoothState); | |
| 196 if (state == BluetoothHeadset.STATE_CONNECTED) { | |
| 197 scoConnectionAttempts = 0; | |
| 198 updateAudioDeviceState(); | |
| 199 } else if (state == BluetoothHeadset.STATE_CONNECTING) { | |
| 200 // No action needed. | |
| 201 } else if (state == BluetoothHeadset.STATE_DISCONNECTING) { | |
| 202 // No action needed. | |
| 203 } else if (state == BluetoothHeadset.STATE_DISCONNECTED) { | |
| 204 // Bluetooth is probably powered off during the call. | |
| 205 stopBluetoothSco(); | |
| 206 updateAudioDeviceState(); | |
| 207 } | |
| 208 // Change in the audio (SCO) connection state of the Headset profile. | |
| 209 } else if (action.equals(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED)) { | |
| 210 final int state = intent.getIntExtra( | |
| 211 BluetoothHeadset.EXTRA_STATE, BluetoothHeadset.STATE_AUDIO_DISCONNEC TED); | |
| 212 final BluetoothDevice device = | |
| 213 (BluetoothDevice) intent.getParcelableExtra(BluetoothDevice.EXTRA_DE VICE); | |
| 214 Log.d(TAG_BT, "BluetoothHeadsetBroadcastReceiver.onReceive: " | |
| 215 + "a=ACTION_AUDIO_STATE_CHANGED, " | |
| 216 + "s=" + stateToString(state) + ", " | |
| 217 + "n=" + device.getName() + ", " | |
| 218 + "sb=" + isInitialStickyBroadcast() + ", " | |
| 219 + "BT state: " + bluetoothState); | |
| 220 if (state == BluetoothHeadset.STATE_AUDIO_CONNECTED) { | |
| 221 cancelBluetoothTimer(); | |
| 222 if (bluetoothState == BluetoothState.SCO_CONNECTING) { | |
| 223 bluetoothState = BluetoothState.SCO_CONNECTED; | |
| 224 scoConnectionAttempts = 0; | |
| 225 updateAudioDeviceState(); | |
| 226 } else { | |
| 227 Log.w(TAG_BT, "Unexpected state BluetoothHeadset.STATE_AUDIO_CONNECT ED"); | |
| 228 } | |
| 229 } else if (state == BluetoothHeadset.STATE_AUDIO_CONNECTING) { | |
| 230 // No action needed. | |
| 231 } else if (state == BluetoothHeadset.STATE_AUDIO_DISCONNECTED) { | |
| 232 if (isInitialStickyBroadcast()) { | |
| 233 Log.d(TAG_BT, "Ignore STATE_AUDIO_DISCONNECTED initial sticky broadc ast."); | |
| 234 return; | |
| 235 } | |
| 236 stopBluetoothSco(); | |
| 237 updateAudioDeviceState(); | |
| 238 } | |
| 239 } | |
| 240 } | |
| 241 }; | |
| 242 | |
| 243 // Implementation of an interface that notifies BluetoothProfile IPC clients w hen they have been | |
| 244 // connected to or disconnected from the service. | |
| 245 private class BluetoothServiceListener implements BluetoothProfile.ServiceList ener { | |
| 246 @Override | |
| 247 // Called to notify the client when the proxy object has been connected to t he service. | |
| 248 public void onServiceConnected(int profile, BluetoothProfile proxy) { | |
| 249 if (profile != BluetoothProfile.HEADSET || bluetoothState == BluetoothStat e.UNINITIALIZED) { | |
| 250 return; | |
| 251 } | |
| 252 Log.d(TAG_BT, "BluetoothServiceListener.onServiceConnected: BT state=" + b luetoothState); | |
| 253 // Android only supports one connected Bluetooth Headset at a time. | |
| 254 bluetoothHeadset = (BluetoothHeadset) proxy; | |
| 255 // Update BT state and list of available audio devices. | |
| 256 updateAudioDeviceState(); | |
| 86 } | 257 } |
| 87 | 258 |
| 88 // The proximity sensor should only be activated when there are exactly two | 259 @Override |
| 89 // available audio devices. | 260 // Called to notify the client that this proxy object has been disconnected from the service. |
| 90 if (audioDevices.size() == 2 && audioDevices.contains(AppRTCAudioManager.Aud ioDevice.EARPIECE) | 261 public void onServiceDisconnected(int profile) { |
| 91 && audioDevices.contains(AppRTCAudioManager.AudioDevice.SPEAKER_PHONE)) { | 262 if (profile != BluetoothProfile.HEADSET || bluetoothState == BluetoothStat e.UNINITIALIZED) { |
| 92 if (proximitySensor.sensorReportsNearState()) { | 263 return; |
| 93 // Sensor reports that a "handset is being held up to a person's ear", | |
| 94 // or "something is covering the light sensor". | |
| 95 setAudioDevice(AppRTCAudioManager.AudioDevice.EARPIECE); | |
| 96 } else { | |
| 97 // Sensor reports that a "handset is removed from a person's ear", or | |
| 98 // "the light sensor is no longer covered". | |
| 99 setAudioDevice(AppRTCAudioManager.AudioDevice.SPEAKER_PHONE); | |
| 100 } | 264 } |
| 265 Log.d(TAG_BT, "BluetoothServiceListener.onServiceDisconnected: BT state=" + bluetoothState); | |
| 266 // stopBluetoothSco(); | |
| 267 bluetoothHeadset = null; | |
| 268 // bluetoothDevice = null; | |
| 269 bluetoothState = BluetoothState.HEADSET_UNAVAILABLE; | |
| 270 // Update BT state and list of available audio devices. | |
| 271 updateAudioDeviceState(); | |
| 101 } | 272 } |
| 102 } | 273 } |
| 103 | 274 |
| 104 /** Construction */ | 275 // Construction. |
| 105 static AppRTCAudioManager create(Context context, Runnable deviceStateChangeLi stener) { | 276 static AppRTCAudioManager create(Context context) { |
| 106 return new AppRTCAudioManager(context, deviceStateChangeListener); | 277 return new AppRTCAudioManager(context); |
| 107 } | 278 } |
| 108 | 279 |
| 109 private AppRTCAudioManager(Context context, Runnable deviceStateChangeListener ) { | 280 private AppRTCAudioManager(Context context) { |
| 281 Log.d(TAG, "ctor"); | |
| 110 apprtcContext = context; | 282 apprtcContext = context; |
| 111 onStateChangeListener = deviceStateChangeListener; | |
| 112 audioManager = ((AudioManager) context.getSystemService(Context.AUDIO_SERVIC E)); | 283 audioManager = ((AudioManager) context.getSystemService(Context.AUDIO_SERVIC E)); |
| 284 wiredHeadsetReceiver = new WiredHeadsetReceiver(); | |
| 285 bluetoothServiceListener = new BluetoothServiceListener(); | |
| 286 bluetoothHeadsetReceiver = new BluetoothHeadsetBroadcastReceiver(); | |
| 287 amState = AudioManagerState.UNINITIALIZED; | |
| 288 | |
| 289 final HandlerThread handlerThread = new HandlerThread(TAG); | |
| 290 handlerThread.start(); | |
|
magjed_webrtc
2016/11/28 12:59:45
It looks like you are only using this thread for a
henrika_webrtc
2016/11/28 14:06:18
Will see if I can resolve in ant alternative way.
magjed_webrtc
2016/11/29 19:59:52
I thought you would do more than just a timeout. J
| |
| 291 handler = new Handler(handlerThread.getLooper()); | |
| 113 | 292 |
| 114 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPref erences(context); | 293 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPref erences(context); |
| 115 useSpeakerphone = sharedPreferences.getString(context.getString(R.string.pre f_speakerphone_key), | 294 useSpeakerphone = sharedPreferences.getString(context.getString(R.string.pre f_speakerphone_key), |
| 116 context.getString(R.string.pref_speakerphone_default)); | 295 context.getString(R.string.pref_speakerphone_default)); |
| 117 | 296 Log.d(TAG, "useSpeakerphone: " + useSpeakerphone); |
| 118 if (useSpeakerphone.equals(SPEAKERPHONE_FALSE)) { | 297 if (useSpeakerphone.equals(SPEAKERPHONE_FALSE)) { |
| 119 defaultAudioDevice = AudioDevice.EARPIECE; | 298 defaultAudioDevice = AudioDevice.EARPIECE; |
| 120 } else { | 299 } else { |
| 121 defaultAudioDevice = AudioDevice.SPEAKER_PHONE; | 300 defaultAudioDevice = AudioDevice.SPEAKER_PHONE; |
| 122 } | 301 } |
| 302 Log.d(TAG, "defaultAudioDevice: " + defaultAudioDevice); | |
| 303 AppRTCUtils.logDeviceInfo(TAG); | |
| 123 | 304 |
| 124 // Create and initialize the proximity sensor. | 305 // Check if this process has the BLUETOOTH permission or not. |
| 125 // Tablet devices (e.g. Nexus 7) does not support proximity sensors. | 306 hasBluetoothPermission = hasPermission(android.Manifest.permission.BLUETOOTH ); |
| 126 // Note that, the sensor will not be active until start() has been called. | 307 Log.d(TAG_BT, "hasBluetoothPermission: " + hasBluetoothPermission); |
| 127 proximitySensor = AppRTCProximitySensor.create(context, new Runnable() { | |
| 128 // This method will be called each time a state change is detected. | |
| 129 // Example: user holds his hand over the device (closer than ~5 cm), | |
| 130 // or removes his hand from the device. | |
| 131 public void run() { | |
| 132 onProximitySensorChangedState(); | |
| 133 } | |
| 134 }); | |
| 135 AppRTCUtils.logDeviceInfo(TAG); | |
| 136 } | 308 } |
| 137 | 309 |
| 138 public void init() { | 310 public void start(AudioManagerEvents audioManagerEvents) { |
| 139 Log.d(TAG, "init"); | 311 Log.d(TAG, "start"); |
| 140 if (initialized) { | 312 if (amState == AudioManagerState.RUNNING) { |
| 313 Log.e(TAG, "AudioManager is already active"); | |
| 141 return; | 314 return; |
| 142 } | 315 } |
| 316 // TODO(henrika): perhaps call new method called preInitAudio() here if UNIN ITIALIZED. | |
| 143 | 317 |
| 144 // Store current audio state so we can restore it when close() is called. | 318 Log.d(TAG, "AudioManager starts..."); |
| 319 this.audioManagerEvents = audioManagerEvents; | |
| 320 amState = AudioManagerState.RUNNING; | |
| 321 | |
| 322 // Store current audio state so we can restore it when stop() is called. | |
| 145 savedAudioMode = audioManager.getMode(); | 323 savedAudioMode = audioManager.getMode(); |
| 146 savedIsSpeakerPhoneOn = audioManager.isSpeakerphoneOn(); | 324 savedIsSpeakerPhoneOn = audioManager.isSpeakerphoneOn(); |
| 147 savedIsMicrophoneMute = audioManager.isMicrophoneMute(); | 325 savedIsMicrophoneMute = audioManager.isMicrophoneMute(); |
| 326 hasWiredHeadset = hasWiredHeadset(); | |
| 148 | 327 |
| 149 // Create an AudioManager.OnAudioFocusChangeListener instance. | 328 // Create an AudioManager.OnAudioFocusChangeListener instance. |
| 150 audioFocusChangeListener = new AudioManager.OnAudioFocusChangeListener() { | 329 audioFocusChangeListener = new AudioManager.OnAudioFocusChangeListener() { |
| 151 // Called on the listener to notify if the audio focus for this listener h as been changed. | 330 // Called on the listener to notify if the audio focus for this listener h as been changed. |
| 152 // The |focusChange| value indicates whether the focus was gained, whether the focus was lost, | 331 // The |focusChange| value indicates whether the focus was gained, whether the focus was lost, |
| 153 // and whether that loss is transient, or whether the new focus holder wil l hold it for an | 332 // and whether that loss is transient, or whether the new focus holder wil l hold it for an |
| 154 // unknown amount of time. | 333 // unknown amount of time. |
| 155 // TODO(henrika): possibly extend support of handling audio-focus changes. Only contains | 334 // TODO(henrika): possibly extend support of handling audio-focus changes. Only contains |
| 156 // logging for now. | 335 // logging for now. |
| 157 @Override | 336 @Override |
| (...skipping 34 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 192 AudioManager.STREAM_VOICE_CALL, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT); | 371 AudioManager.STREAM_VOICE_CALL, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT); |
| 193 if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { | 372 if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { |
| 194 Log.d(TAG, "Audio focus request granted for VOICE_CALL streams"); | 373 Log.d(TAG, "Audio focus request granted for VOICE_CALL streams"); |
| 195 } else { | 374 } else { |
| 196 Log.e(TAG, "Audio focus request failed"); | 375 Log.e(TAG, "Audio focus request failed"); |
| 197 } | 376 } |
| 198 | 377 |
| 199 // Start by setting MODE_IN_COMMUNICATION as default audio mode. It is | 378 // Start by setting MODE_IN_COMMUNICATION as default audio mode. It is |
| 200 // required to be in this mode when playout and/or recording starts for | 379 // required to be in this mode when playout and/or recording starts for |
| 201 // best possible VoIP performance. | 380 // best possible VoIP performance. |
| 202 // TODO(henrika): we migh want to start with RINGTONE mode here instead. | |
| 203 audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION); | 381 audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION); |
| 204 | 382 |
| 205 // Always disable microphone mute during a WebRTC call. | 383 // Always disable microphone mute during a WebRTC call. |
| 206 setMicrophoneMute(false); | 384 setMicrophoneMute(false); |
| 207 | 385 |
| 386 // Set initial device states. | |
| 387 userSelectedAudioDevice = AudioDevice.NONE; | |
| 388 selectedAudioDevice = AudioDevice.NONE; | |
| 389 audioDevices.clear(); | |
| 390 | |
| 391 // Initialize and start Bluetooth if a BT device is available or initiate | |
| 392 // detection of new (enabled) BT devices. | |
| 393 bluetoothState = BluetoothState.UNINITIALIZED; | |
| 394 startBluetooth(); | |
| 395 | |
| 208 // Do initial selection of audio device. This setting can later be changed | 396 // Do initial selection of audio device. This setting can later be changed |
| 209 // either by adding/removing a wired headset or by covering/uncovering the | 397 // either by adding/removing a BT or wired headset or by covering/uncovering |
| 210 // proximity sensor. | 398 // the proximity sensor. |
| 211 updateAudioDeviceState(hasWiredHeadset()); | 399 updateAudioDeviceState(); |
| 212 | 400 |
| 213 // Register receiver for broadcast intents related to adding/removing a | 401 // Register receiver for broadcast intents related to adding/removing a |
| 214 // wired headset (Intent.ACTION_HEADSET_PLUG). | 402 // wired headset. |
| 215 registerForWiredHeadsetIntentBroadcast(); | 403 registerReceiver(wiredHeadsetReceiver, new IntentFilter(Intent.ACTION_HEADSE T_PLUG)); |
| 216 | 404 |
| 217 initialized = true; | 405 IntentFilter bluetoothHeadsetFilter = new IntentFilter(); |
| 406 // Register receiver for change in connection state of the Headset profile. | |
| 407 bluetoothHeadsetFilter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CH ANGED); | |
| 408 // Register receiver for change in audio connection state of the Headset pro file. | |
| 409 bluetoothHeadsetFilter.addAction(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED ); | |
| 410 registerReceiver(bluetoothHeadsetReceiver, bluetoothHeadsetFilter); | |
| 411 Log.d(TAG, "AudioManager started"); | |
| 218 } | 412 } |
| 219 | 413 |
| 220 public void close() { | 414 public void stop() { |
| 221 Log.d(TAG, "close"); | 415 Log.d(TAG, "stop"); |
| 222 if (!initialized) { | 416 if (amState != AudioManagerState.RUNNING) { |
| 417 Log.e(TAG, "Trying to stop AudioManager in incorrect state: " + amState); | |
| 223 return; | 418 return; |
| 224 } | 419 } |
| 420 amState = AudioManagerState.UNINITIALIZED; | |
| 225 | 421 |
| 226 unregisterForWiredHeadsetIntentBroadcast(); | 422 // Cancel Bluetooth related tasks. |
| 423 unregisterReceiver(bluetoothHeadsetReceiver); | |
| 424 unregisterReceiver(wiredHeadsetReceiver); | |
| 425 stopBluetooth(); | |
| 227 | 426 |
| 228 // Restore previously stored audio states. | 427 // Restore previously stored audio states. |
| 229 setSpeakerphoneOn(savedIsSpeakerPhoneOn); | 428 setSpeakerphoneOn(savedIsSpeakerPhoneOn); |
| 230 setMicrophoneMute(savedIsMicrophoneMute); | 429 setMicrophoneMute(savedIsMicrophoneMute); |
| 231 audioManager.setMode(savedAudioMode); | 430 audioManager.setMode(savedAudioMode); |
| 232 | 431 |
| 233 // Abandon audio focus. Gives the previous focus owner, if any, focus. | 432 // Abandon audio focus. Gives the previous focus owner, if any, focus. |
| 234 audioManager.abandonAudioFocus(audioFocusChangeListener); | 433 audioManager.abandonAudioFocus(audioFocusChangeListener); |
| 235 audioFocusChangeListener = null; | 434 audioFocusChangeListener = null; |
| 236 Log.d(TAG, "Abandoned audio focus for VOICE_CALL streams"); | 435 Log.d(TAG, "Abandoned audio focus for VOICE_CALL streams"); |
| 237 | 436 |
| 238 if (proximitySensor != null) { | 437 audioManagerEvents = null; |
| 239 proximitySensor.stop(); | 438 Log.d(TAG, "AudioManager stopped"); |
| 240 proximitySensor = null; | |
| 241 } | |
| 242 | |
| 243 initialized = false; | |
| 244 } | 439 } |
| 245 | 440 |
| 246 /** Changes selection of the currently active audio device. */ | 441 // Changes selection of the currently active audio device. |
| 247 public void setAudioDevice(AudioDevice device) { | 442 private void setAudioDeviceInternal(AudioDevice device) { |
| 248 Log.d(TAG, "setAudioDevice(device=" + device + ")"); | 443 Log.d(TAG, "setAudioDeviceInternal(device=" + device + ")"); |
| 249 AppRTCUtils.assertIsTrue(audioDevices.contains(device)); | 444 AppRTCUtils.assertIsTrue(audioDevices.contains(device)); |
| 250 | 445 |
| 251 switch (device) { | 446 switch (device) { |
| 252 case SPEAKER_PHONE: | 447 case SPEAKER_PHONE: |
| 253 setSpeakerphoneOn(true); | 448 setSpeakerphoneOn(true); |
| 254 selectedAudioDevice = AudioDevice.SPEAKER_PHONE; | |
| 255 break; | 449 break; |
| 256 case EARPIECE: | 450 case EARPIECE: |
| 257 setSpeakerphoneOn(false); | 451 setSpeakerphoneOn(false); |
| 258 selectedAudioDevice = AudioDevice.EARPIECE; | |
| 259 break; | 452 break; |
| 260 case WIRED_HEADSET: | 453 case WIRED_HEADSET: |
| 261 setSpeakerphoneOn(false); | 454 setSpeakerphoneOn(false); |
| 262 selectedAudioDevice = AudioDevice.WIRED_HEADSET; | 455 break; |
| 456 case BLUETOOTH: | |
| 457 setSpeakerphoneOn(false); | |
| 263 break; | 458 break; |
| 264 default: | 459 default: |
| 265 Log.e(TAG, "Invalid audio device selection"); | 460 Log.e(TAG, "Invalid audio device selection"); |
| 266 break; | 461 break; |
| 267 } | 462 } |
| 268 onAudioManagerChangedState(); | 463 selectedAudioDevice = device; |
| 269 } | 464 } |
| 270 | 465 |
| 271 /** Returns current set of available/selectable audio devices. */ | 466 // Changes default audio device. |
| 272 public Set<AudioDevice> getAudioDevices() { | 467 // TODO(henrika): add usage of this method in the AppRTCMobile client. |
| 468 public synchronized void setDefaultAudioDevice(AudioDevice defaultDevice) { | |
| 469 switch (defaultDevice) { | |
| 470 case SPEAKER_PHONE: | |
| 471 defaultAudioDevice = defaultDevice; | |
| 472 break; | |
| 473 case EARPIECE: | |
| 474 if (hasEarpiece()) { | |
| 475 defaultAudioDevice = defaultDevice; | |
| 476 } else { | |
| 477 defaultAudioDevice = AudioDevice.SPEAKER_PHONE; | |
| 478 } | |
| 479 break; | |
| 480 default: | |
| 481 Log.e(TAG, "Invalid default audio device selection"); | |
| 482 break; | |
| 483 } | |
| 484 Log.d(TAG, "setDefaultAudioDevice(device=" + defaultAudioDevice + ")"); | |
| 485 updateAudioDeviceState(); | |
| 486 } | |
| 487 | |
| 488 // Changes selection of the currently active audio device. | |
| 489 public synchronized void selectAudioDevice(AudioDevice device) { | |
| 490 if (!audioDevices.contains(device)) { | |
| 491 Log.e(TAG, "Can not select " + device + " from available " + audioDevices) ; | |
| 492 } | |
| 493 userSelectedAudioDevice = device; | |
| 494 updateAudioDeviceState(); | |
| 495 } | |
| 496 | |
| 497 // Returns current set of available/selectable audio devices. | |
| 498 public synchronized Set<AudioDevice> getAudioDevices() { | |
| 273 return Collections.unmodifiableSet(new HashSet<AudioDevice>(audioDevices)); | 499 return Collections.unmodifiableSet(new HashSet<AudioDevice>(audioDevices)); |
| 274 } | 500 } |
| 275 | 501 |
| 276 /** Returns the currently selected audio device. */ | 502 // Returns the currently selected audio device. |
| 277 public AudioDevice getSelectedAudioDevice() { | 503 public synchronized AudioDevice getSelectedAudioDevice() { |
| 278 return selectedAudioDevice; | 504 return selectedAudioDevice; |
| 279 } | 505 } |
| 280 | 506 |
| 281 /** | 507 // Helper method for receiver registration. |
| 282 * Registers receiver for the broadcasted intent when a wired headset is | 508 private void registerReceiver(BroadcastReceiver receiver, IntentFilter filter) { |
| 283 * plugged in or unplugged. The received intent will have an extra | 509 apprtcContext.registerReceiver(receiver, filter); |
| 284 * 'state' value where 0 means unplugged, and 1 means plugged. | |
| 285 */ | |
| 286 private void registerForWiredHeadsetIntentBroadcast() { | |
| 287 IntentFilter filter = new IntentFilter(Intent.ACTION_HEADSET_PLUG); | |
| 288 | |
| 289 /** Receiver which handles changes in wired headset availability. */ | |
| 290 wiredHeadsetReceiver = new BroadcastReceiver() { | |
| 291 private static final int STATE_UNPLUGGED = 0; | |
| 292 private static final int STATE_PLUGGED = 1; | |
| 293 private static final int HAS_NO_MIC = 0; | |
| 294 private static final int HAS_MIC = 1; | |
| 295 | |
| 296 @Override | |
| 297 public void onReceive(Context context, Intent intent) { | |
| 298 int state = intent.getIntExtra("state", STATE_UNPLUGGED); | |
| 299 int microphone = intent.getIntExtra("microphone", HAS_NO_MIC); | |
| 300 String name = intent.getStringExtra("name"); | |
| 301 Log.d(TAG, "BroadcastReceiver.onReceive" + AppRTCUtils.getThreadInfo() + ": " | |
| 302 + "a=" + intent.getAction() + ", s=" | |
| 303 + (state == STATE_UNPLUGGED ? "unplugged" : "plugged") + ", m=" | |
| 304 + (microphone == HAS_MIC ? "mic" : "no mic") + ", n=" + name + " , sb=" | |
| 305 + isInitialStickyBroadcast()); | |
| 306 | |
| 307 boolean hasWiredHeadset = (state == STATE_PLUGGED); | |
| 308 switch (state) { | |
| 309 case STATE_UNPLUGGED: | |
| 310 updateAudioDeviceState(hasWiredHeadset); | |
| 311 break; | |
| 312 case STATE_PLUGGED: | |
| 313 if (selectedAudioDevice != AudioDevice.WIRED_HEADSET) { | |
| 314 updateAudioDeviceState(hasWiredHeadset); | |
| 315 } | |
| 316 break; | |
| 317 default: | |
| 318 Log.e(TAG, "Invalid state"); | |
| 319 break; | |
| 320 } | |
| 321 } | |
| 322 }; | |
| 323 | |
| 324 apprtcContext.registerReceiver(wiredHeadsetReceiver, filter); | |
| 325 } | 510 } |
| 326 | 511 |
| 327 /** Unregister receiver for broadcasted ACTION_HEADSET_PLUG intent. */ | 512 // Helper method for unregistration of an existing receiver. |
| 328 private void unregisterForWiredHeadsetIntentBroadcast() { | 513 private void unregisterReceiver(BroadcastReceiver receiver) { |
| 329 apprtcContext.unregisterReceiver(wiredHeadsetReceiver); | 514 apprtcContext.unregisterReceiver(receiver); |
| 330 wiredHeadsetReceiver = null; | |
| 331 } | 515 } |
| 332 | 516 |
| 333 /** Sets the speaker phone mode. */ | 517 // Sets the speaker phone mode. |
| 334 private void setSpeakerphoneOn(boolean on) { | 518 private void setSpeakerphoneOn(boolean on) { |
| 335 boolean wasOn = audioManager.isSpeakerphoneOn(); | 519 boolean wasOn = audioManager.isSpeakerphoneOn(); |
| 336 if (wasOn == on) { | 520 if (wasOn == on) { |
| 337 return; | 521 return; |
| 338 } | 522 } |
| 339 audioManager.setSpeakerphoneOn(on); | 523 audioManager.setSpeakerphoneOn(on); |
| 340 } | 524 } |
| 341 | 525 |
| 342 /** Sets the microphone mute state. */ | 526 // Sets the microphone mute state. |
| 343 private void setMicrophoneMute(boolean on) { | 527 private void setMicrophoneMute(boolean on) { |
| 344 boolean wasMuted = audioManager.isMicrophoneMute(); | 528 boolean wasMuted = audioManager.isMicrophoneMute(); |
| 345 if (wasMuted == on) { | 529 if (wasMuted == on) { |
| 346 return; | 530 return; |
| 347 } | 531 } |
| 348 audioManager.setMicrophoneMute(on); | 532 audioManager.setMicrophoneMute(on); |
| 349 } | 533 } |
| 350 | 534 |
| 351 /** Gets the current earpiece state. */ | 535 // Starts all Bluetooth related tasks. |
| 536 void startBluetooth() { | |
| 537 Log.d(TAG_BT, "startBluetooth"); | |
| 538 if (!hasBluetoothPermission) { | |
| 539 Log.w(TAG_BT, "Process lacks BLUETOOTH permission"); | |
| 540 return; | |
| 541 } | |
| 542 if (bluetoothState != BluetoothState.UNINITIALIZED) { | |
| 543 Log.w(TAG_BT, "Invalid BT state"); | |
| 544 return; | |
| 545 } | |
| 546 bluetoothHeadset = null; | |
| 547 bluetoothDevice = null; | |
| 548 scoConnectionAttempts = 0; | |
| 549 // Get a handle to the default local Bluetooth adapter. | |
| 550 bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); | |
| 551 if (bluetoothAdapter == null) { | |
| 552 Log.w(TAG_BT, "Device does not support Bluetooth"); | |
| 553 return; | |
| 554 } | |
| 555 // Ensure that the device supports use of BT SCO audio for off call use case s. | |
| 556 if (!audioManager.isBluetoothScoAvailableOffCall()) { | |
| 557 Log.e(TAG_BT, "Bluetooth SCO audio is not available off call"); | |
| 558 return; | |
| 559 } | |
| 560 logBluetoothAdapterInfo(bluetoothAdapter); | |
| 561 // Establish a connection to the HEADSET profile (includes both Bluetooth He adset and | |
| 562 // Hands-Free) proxy object and install a listener. | |
| 563 if (!bluetoothAdapter.getProfileProxy( | |
| 564 apprtcContext, bluetoothServiceListener, BluetoothProfile.HEADSET)) { | |
| 565 Log.e(TAG_BT, "BluetoothAdapter.getProfileProxy(HEADSET) failed"); | |
| 566 return; | |
| 567 } | |
| 568 Log.d(TAG_BT, "HEADSET profile state: " | |
| 569 + stateToString(bluetoothAdapter.getProfileConnectionState(Bluetooth Profile.HEADSET))); | |
| 570 Log.d(TAG_BT, "Bluetooth proxy for headset profile has started"); | |
| 571 bluetoothState = BluetoothState.HEADSET_UNAVAILABLE; | |
| 572 } | |
| 573 | |
| 574 // Stops all Bluetooth tasks. | |
| 575 void stopBluetooth() { | |
| 576 Log.d(TAG_BT, "stopBluetooth: BT state=" + bluetoothState); | |
| 577 if (bluetoothAdapter != null) { | |
| 578 // Stop BT SCO connection with remote device if needed. | |
| 579 stopBluetoothSco(); | |
| 580 // Close down remaining BT resources. | |
| 581 if (bluetoothState != BluetoothState.UNINITIALIZED) { | |
| 582 cancelBluetoothTimer(); | |
| 583 if (bluetoothHeadset != null) { | |
| 584 bluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, bluetooth Headset); | |
| 585 bluetoothHeadset = null; | |
| 586 } | |
| 587 bluetoothAdapter = null; | |
| 588 bluetoothDevice = null; | |
| 589 bluetoothState = BluetoothState.UNINITIALIZED; | |
| 590 } | |
| 591 } | |
| 592 } | |
| 593 | |
| 594 // Called when start of the BT SCO channel takes too long time. Usually | |
| 595 // happens when the BT device has been turned on during an ongoing call. | |
| 596 private void bluetoothTimeout() { | |
| 597 if (amState != AudioManagerState.RUNNING || bluetoothState == BluetoothState .UNINITIALIZED | |
| 598 || bluetoothHeadset == null) { | |
| 599 return; | |
| 600 } | |
| 601 Log.d(TAG_BT, "bluetoothTimeout: BT state=" + bluetoothState); | |
| 602 if (bluetoothState != BluetoothState.SCO_CONNECTING) { | |
| 603 return; | |
| 604 } | |
| 605 // Bluetooth SCO should be connecting; check the latest result. | |
| 606 boolean scoConnected = false; | |
| 607 List<BluetoothDevice> devices = bluetoothHeadset.getConnectedDevices(); | |
| 608 if (devices.size() > 0) { | |
| 609 bluetoothDevice = devices.get(0); | |
| 610 if (bluetoothHeadset.isAudioConnected(bluetoothDevice)) { | |
| 611 Log.d(TAG_BT, "SCO connected with " + bluetoothDevice.getName()); | |
| 612 scoConnected = true; | |
| 613 } else { | |
| 614 Log.d(TAG_BT, "SCO is not connected with " + bluetoothDevice.getName()); | |
| 615 } | |
| 616 } | |
| 617 | |
| 618 if (scoConnected) { | |
| 619 // We thought BT had timed out, but it's actually on; updating state. | |
| 620 bluetoothState = BluetoothState.SCO_CONNECTED; | |
| 621 scoConnectionAttempts = 0; | |
| 622 updateAudioDeviceState(); | |
| 623 } else { | |
| 624 // Give up and "cancel" our request by calling stopBluetoothSco(). | |
| 625 Log.w(TAG_BT, "BT failed to connect after timeout"); | |
| 626 stopBluetoothSco(); | |
| 627 updateAudioDeviceState(); | |
| 628 } | |
| 629 } | |
| 630 | |
| 631 private void startBluetoothTimer() { | |
| 632 Log.d(TAG_BT, "startBluetoothTimer"); | |
| 633 handler.postDelayed(bluetoothTimeoutRunnable, BLUETOOTH_SCO_TIMEOUT_MS); | |
| 634 } | |
| 635 | |
| 636 private void cancelBluetoothTimer() { | |
| 637 Log.d(TAG_BT, "cancelBluetoothTimer"); | |
| 638 handler.removeCallbacks(bluetoothTimeoutRunnable); | |
| 639 } | |
| 640 | |
| 641 // Starts Bluetooth SCO connection with remote device. | |
| 642 // Note that the phone application always has the priority on the usage of the SCO connection | |
| 643 // for telephony. If this method is called while the phone is in call it will be ignored. | |
| 644 // Similarly, if a call is received or sent while an application is using the SCO connection, | |
| 645 // the connection will be lost for the application and NOT returned automatica lly when the call | |
| 646 // ends. Also note that: up to and including API version JELLY_BEAN_MR1, this method initiates a | |
| 647 // virtual voice call to the Bluetooth headset. After API version JELLY_BEAN_M R2 only a raw SCO | |
| 648 // audio connection is established. | |
| 649 // TODO(henrika): should we add support for virtual voice call to BT headset a lso for JBMR2 and | |
| 650 // higher. It might be required to initiates a virtual voice call since many d evices do not | |
| 651 // accept SCO audio without a "call". | |
| 652 private void startBluetoothSco() { | |
| 653 if (bluetoothState != BluetoothState.HEADSET_AVAILABLE) { | |
| 654 return; | |
| 655 } | |
| 656 Log.d(TAG_BT, "startBluetoothSco: BT state=" + bluetoothState + ", " | |
| 657 + "attempt: " + scoConnectionAttempts + ", " | |
| 658 + "SCO is on: " + audioManager.isBluetoothScoOn()); | |
| 659 // The SCO connection establishment can take several seconds, hence we canno t rely on the | |
| 660 // connection to be available when the method returns but instead register t o receive the | |
| 661 // intent ACTION_SCO_AUDIO_STATE_UPDATED and wait for the state to be SCO_AU DIO_STATE_CONNECTED. | |
| 662 bluetoothState = BluetoothState.SCO_CONNECTING; | |
| 663 audioManager.startBluetoothSco(); | |
| 664 scoConnectionAttempts++; | |
| 665 startBluetoothTimer(); | |
| 666 } | |
| 667 | |
| 668 // Stops Bluetooth SCO connection with remote device. | |
| 669 private void stopBluetoothSco() { | |
| 670 if (bluetoothState != BluetoothState.SCO_CONNECTING | |
| 671 && bluetoothState != BluetoothState.SCO_CONNECTED) { | |
| 672 return; | |
| 673 } | |
| 674 Log.d(TAG_BT, "stopBluetoothSco: BT state=" + bluetoothState + ", " | |
| 675 + "SCO is on: " + audioManager.isBluetoothScoOn()); | |
| 676 cancelBluetoothTimer(); | |
| 677 audioManager.stopBluetoothSco(); | |
| 678 bluetoothState = BluetoothState.SCO_DISCONNECTING; | |
| 679 } | |
| 680 | |
| 681 // Use the BluetoothHeadset proxy object (controls the Bluetooth Headset Servi ce via IPC) to | |
| 682 // update the list of connected devices for the HEADSET profile. | |
| 683 private void updateBluetoothDevices() { | |
| 684 if (amState != AudioManagerState.RUNNING || bluetoothState == BluetoothState .UNINITIALIZED | |
| 685 || bluetoothHeadset == null) { | |
| 686 return; | |
| 687 } | |
| 688 Log.d(TAG_BT, "updateBluetoothDevices"); | |
| 689 // Update the Bluetooth device list for the headset profile. | |
| 690 // The BluetoothDevice class is just a thin wrapper for a Bluetooth hardware address. | |
| 691 List<BluetoothDevice> devices = bluetoothHeadset.getConnectedDevices(); | |
| 692 if (devices.size() == 0) { | |
| 693 bluetoothDevice = null; | |
| 694 } else { | |
| 695 bluetoothDevice = devices.get(0); | |
| 696 } | |
| 697 if (bluetoothDevice == null) { | |
| 698 bluetoothState = BluetoothState.HEADSET_UNAVAILABLE; | |
| 699 Log.d(TAG_BT, "No connected bluetooth headset"); | |
| 700 } else { | |
| 701 bluetoothState = BluetoothState.HEADSET_AVAILABLE; | |
| 702 Log.d(TAG_BT, "Connected bluetooth headset: " | |
| 703 + "name=" + bluetoothDevice.getName() + ", " | |
| 704 + "state=" + stateToString(bluetoothHeadset.getConnectionState(blu etoothDevice)) | |
| 705 + ", SCO audio=" + bluetoothHeadset.isAudioConnected(bluetoothDevi ce)); | |
| 706 } | |
| 707 Log.d(TAG_BT, "Updated Bluetooth state: " + bluetoothState); | |
| 708 } | |
| 709 | |
| 710 // Gets the current earpiece state. | |
| 352 private boolean hasEarpiece() { | 711 private boolean hasEarpiece() { |
| 353 return apprtcContext.getPackageManager().hasSystemFeature(PackageManager.FEA TURE_TELEPHONY); | 712 return apprtcContext.getPackageManager().hasSystemFeature(PackageManager.FEA TURE_TELEPHONY); |
| 354 } | 713 } |
| 355 | 714 |
| 356 /** | 715 // Checks whether a wired headset is connected or not. |
| 357 * Checks whether a wired headset is connected or not. | 716 // This is not a valid indication that audio playback is actually over |
| 358 * This is not a valid indication that audio playback is actually over | 717 // the wired headset as audio routing depends on other conditions. We |
| 359 * the wired headset as audio routing depends on other conditions. We | 718 // only use it as an early indicator (during initialization) of an attached |
| 360 * only use it as an early indicator (during initialization) of an attached | 719 // wired headset. |
| 361 * wired headset. | |
| 362 */ | |
| 363 @Deprecated | 720 @Deprecated |
| 364 private boolean hasWiredHeadset() { | 721 private boolean hasWiredHeadset() { |
| 365 return audioManager.isWiredHeadsetOn(); | 722 return audioManager.isWiredHeadsetOn(); |
| 366 } | 723 } |
| 367 | 724 |
| 368 /** Update list of possible audio devices and make new device selection. */ | 725 // Update list of possible audio devices and make new device selection. |
| 369 private void updateAudioDeviceState(boolean hasWiredHeadset) { | 726 private void updateAudioDeviceState() { |
| 370 // Update the list of available audio devices. | 727 Log.d(TAG, "--- updateAudioDeviceState: " |
| 371 audioDevices.clear(); | 728 + "wired headset=" + hasWiredHeadset + ", " |
| 729 + "BT state=" + bluetoothState); | |
| 730 Log.d(TAG, "Device status: " | |
| 731 + "available=" + audioDevices + ", " | |
| 732 + "selected=" + selectedAudioDevice + ", " | |
| 733 + "user selected=" + userSelectedAudioDevice); | |
| 734 | |
| 735 // Update Bluetooth device list. | |
| 736 if (bluetoothState == BluetoothState.HEADSET_AVAILABLE | |
| 737 || bluetoothState == BluetoothState.HEADSET_UNAVAILABLE | |
| 738 || bluetoothState == BluetoothState.SCO_DISCONNECTING) { | |
| 739 updateBluetoothDevices(); | |
| 740 } | |
| 741 | |
| 742 // Update the set of available audio devices. | |
| 743 Set<AudioDevice> newAudioDevices = new HashSet<>(); | |
| 744 | |
| 745 if (bluetoothState == BluetoothState.SCO_CONNECTED | |
| 746 || bluetoothState == BluetoothState.SCO_CONNECTING | |
| 747 || bluetoothState == BluetoothState.HEADSET_AVAILABLE) { | |
| 748 newAudioDevices.add(AudioDevice.BLUETOOTH); | |
| 749 } | |
| 372 if (hasWiredHeadset) { | 750 if (hasWiredHeadset) { |
| 373 // If a wired headset is connected, then it is the only possible option. | 751 // If a wired headset is connected, then it is the only possible option. |
| 374 audioDevices.add(AudioDevice.WIRED_HEADSET); | 752 newAudioDevices.add(AudioDevice.WIRED_HEADSET); |
| 375 } else { | 753 } else { |
| 376 // No wired headset, hence the audio-device list can contain speaker | 754 // No wired headset, hence the audio-device list can contain speaker |
| 377 // phone (on a tablet), or speaker phone and earpiece (on mobile phone). | 755 // phone (on a tablet), or speaker phone and earpiece (on mobile phone). |
| 378 audioDevices.add(AudioDevice.SPEAKER_PHONE); | 756 newAudioDevices.add(AudioDevice.SPEAKER_PHONE); |
| 379 if (hasEarpiece()) { | 757 if (hasEarpiece()) { |
| 380 audioDevices.add(AudioDevice.EARPIECE); | 758 newAudioDevices.add(AudioDevice.EARPIECE); |
| 381 } | 759 } |
| 382 } | 760 } |
| 383 Log.d(TAG, "audioDevices: " + audioDevices); | 761 |
| 384 | 762 // Update the existing audio device set if needed. |
| 385 // Switch to correct audio device given the list of available audio devices. | 763 boolean audioDeviceSetUpdated = |
| 386 if (hasWiredHeadset) { | 764 !audioDevices.containsAll(newAudioDevices) || !newAudioDevices.containsA ll(audioDevices); |
| 387 setAudioDevice(AudioDevice.WIRED_HEADSET); | 765 if (audioDeviceSetUpdated) { |
| 388 } else { | 766 audioDevices.clear(); |
| 389 setAudioDevice(defaultAudioDevice); | 767 audioDevices.addAll(newAudioDevices); |
| 390 } | 768 } |
| 391 } | 769 |
| 392 | 770 // Correct user selected audio devices if needed. |
| 393 /** Called each time a new audio device has been added or removed. */ | 771 if (bluetoothState == BluetoothState.HEADSET_UNAVAILABLE |
| 394 private void onAudioManagerChangedState() { | 772 && userSelectedAudioDevice == AudioDevice.BLUETOOTH) { |
| 395 Log.d(TAG, "onAudioManagerChangedState: devices=" + audioDevices + ", select ed=" | 773 // If BT is not available, it can't be the user selection. |
| 396 + selectedAudioDevice); | 774 userSelectedAudioDevice = AudioDevice.NONE; |
| 397 | 775 } |
| 398 // Enable the proximity sensor if there are two available audio devices | 776 if (hasWiredHeadset && userSelectedAudioDevice == AudioDevice.SPEAKER_PHONE) { |
| 399 // in the list. Given the current implementation, we know that the choice | 777 // If user selected speaker phone, but then plugged wired headset then mak e |
| 400 // will then be between EARPIECE and SPEAKER_PHONE. | 778 // wired headset as user selected device. |
| 401 if (audioDevices.size() == 2) { | 779 userSelectedAudioDevice = AudioDevice.WIRED_HEADSET; |
| 402 AppRTCUtils.assertIsTrue(audioDevices.contains(AudioDevice.EARPIECE) | 780 } |
| 403 && audioDevices.contains(AudioDevice.SPEAKER_PHONE)); | 781 if (!hasWiredHeadset && userSelectedAudioDevice == AudioDevice.WIRED_HEADSET ) { |
| 404 // Start the proximity sensor. | 782 // If user selected wired headset, but then unplugged wired headset then m ake |
| 405 proximitySensor.start(); | 783 // speaker phone as user selected device. |
| 406 } else if (audioDevices.size() == 1) { | 784 userSelectedAudioDevice = AudioDevice.SPEAKER_PHONE; |
| 407 // Stop the proximity sensor since it is no longer needed. | 785 } |
| 408 proximitySensor.stop(); | 786 |
| 409 } else { | 787 // Need to start Bluetooth if it is available and user either selected it ex plicitly or |
| 410 Log.e(TAG, "Invalid device list"); | 788 // user did not select any output device. |
| 411 } | 789 boolean needBluetoothStart = bluetoothState == BluetoothState.HEADSET_AVAILA BLE |
| 412 | 790 && (userSelectedAudioDevice == AudioDevice.NONE |
| 413 if (onStateChangeListener != null) { | 791 || userSelectedAudioDevice == AudioDevice.BLUETOOTH); |
| 414 // Run callback to notify a listening client. The client can then | 792 |
| 415 // use public getters to query the new state. | 793 // Need to stop Bluetooth if user selected different device and Bluetooth SC O connection is |
| 416 onStateChangeListener.run(); | 794 // established or in the process. |
| 795 boolean needBluetoothStop = (bluetoothState == BluetoothState.SCO_CONNECTED | |
| 796 || bluetoothState == BluetoothState.SCO_CONN ECTING) | |
| 797 && (userSelectedAudioDevice != AudioDevice.NONE | |
| 798 && userSelectedAudioDevice != AudioDevice.BLUETOOTH); | |
| 799 | |
| 800 if (bluetoothState == BluetoothState.HEADSET_AVAILABLE | |
| 801 || bluetoothState == BluetoothState.SCO_CONNECTING | |
| 802 || bluetoothState == BluetoothState.SCO_CONNECTED) { | |
| 803 Log.d(TAG_BT, "Need BT: start=" + needBluetoothStart + ", " | |
| 804 + "stop=" + needBluetoothStop + ", " | |
| 805 + "BT state=" + bluetoothState); | |
| 806 } | |
| 807 | |
| 808 // Start/stop Bluetooth SCO connection | |
| 809 if (needBluetoothStop) { | |
| 810 stopBluetoothSco(); | |
| 811 updateBluetoothDevices(); | |
| 812 } | |
| 813 | |
| 814 if (needBluetoothStart && !needBluetoothStop) { | |
| 815 if (scoConnectionAttempts < MAX_SCO_CONNECTION_ATTEMPTS) { | |
| 816 // Starts BT SCO channel and waits for ACTION_AUDIO_STATE_CHANGED. | |
| 817 Log.d(TAG_BT, "Starting Bluetooth SCO and waits for ACTION_AUDIO_STATE_C HANGED..."); | |
| 818 startBluetoothSco(); | |
| 819 } else { | |
| 820 Log.e(TAG_BT, "BT SCO connection fails - no more attempts"); | |
| 821 audioDevices.remove(AudioDevice.BLUETOOTH); | |
| 822 audioDeviceSetUpdated = true; | |
| 823 } | |
| 824 } | |
| 825 | |
| 826 // Update selected audio device. | |
| 827 AudioDevice newAudioDevice = selectedAudioDevice; | |
| 828 | |
| 829 if (bluetoothState == BluetoothState.SCO_CONNECTED) { | |
| 830 // If a Bluetooth is connected, then it should be used as output audio dev ice. | |
| 831 newAudioDevice = AudioDevice.BLUETOOTH; | |
| 832 } else if (hasWiredHeadset) { | |
| 833 // If a wired headset is connected, but Bluetooth is not, then wired heads et is used as | |
| 834 // output device. | |
| 835 newAudioDevice = AudioDevice.WIRED_HEADSET; | |
| 836 } else { | |
| 837 // No wired headset and no Bluetooth, hence the audio-device list can cont ain speaker | |
| 838 // phone (on a tablet), or speaker phone and earpiece (on mobile phone). | |
| 839 // |defaultAudioDevice| contains either AudioDevice.SPEAKER_PHONE or Audio Device.EARPIECE | |
| 840 // depending on the user's selection. | |
| 841 newAudioDevice = defaultAudioDevice; | |
| 842 } | |
| 843 // Switch to new device but only if there has been any changes. | |
| 844 if (newAudioDevice != selectedAudioDevice || audioDeviceSetUpdated) { | |
| 845 // Do the required device switch. | |
| 846 setAudioDeviceInternal(newAudioDevice); | |
| 847 Log.d(TAG, "New device status: " | |
| 848 + "available=" + audioDevices + ", " | |
| 849 + "selected=" + newAudioDevice); | |
| 850 if (audioManagerEvents != null) { | |
| 851 // Notify a listening client that audio device has been changed. | |
| 852 audioManagerEvents.onAudioDeviceChanged(selectedAudioDevice, audioDevice s); | |
| 853 } | |
| 854 } | |
| 855 Log.d(TAG, "--- updateAudioDeviceState done"); | |
| 856 } | |
| 857 | |
| 858 // Checks if the process has as specified permission or not. | |
| 859 private boolean hasPermission(String permission) { | |
| 860 return apprtcContext.checkPermission(permission, Process.myPid(), Process.my Uid()) | |
| 861 == PackageManager.PERMISSION_GRANTED; | |
| 862 } | |
| 863 | |
| 864 private String stateToString(int state) { | |
| 865 String stateString; | |
| 866 switch (state) { | |
| 867 case BluetoothAdapter.STATE_DISCONNECTED: | |
| 868 // The profile is in disconnected state. | |
| 869 stateString = "DISCONNECTED"; | |
| 870 break; | |
| 871 case BluetoothAdapter.STATE_CONNECTED: | |
| 872 // The profile is in connected state. | |
| 873 stateString = "CONNECTED"; | |
| 874 break; | |
| 875 case BluetoothAdapter.STATE_CONNECTING: | |
| 876 // The profile is in connecting state. | |
| 877 stateString = "CONNECTING"; | |
| 878 break; | |
| 879 case BluetoothAdapter.STATE_DISCONNECTING: | |
| 880 // The profile is in disconnecting state. | |
| 881 stateString = "DISCONNECTING"; | |
| 882 break; | |
| 883 case BluetoothAdapter.STATE_OFF: | |
| 884 // Indicates the local Bluetooth adapter is off. | |
| 885 stateString = "OFF"; | |
| 886 break; | |
| 887 case BluetoothAdapter.STATE_ON: | |
| 888 // Indicates the local Bluetooth adapter is on, and ready for use. | |
| 889 stateString = "ON"; | |
| 890 break; | |
| 891 case BluetoothAdapter.STATE_TURNING_OFF: | |
| 892 // Indicates the local Bluetooth adapter is turning off. Local clients s hould immediately | |
| 893 // attempt graceful disconnection of any remote links. | |
| 894 stateString = "TURNING_OFF"; | |
| 895 break; | |
| 896 case BluetoothAdapter.STATE_TURNING_ON: | |
| 897 // Indicates the local Bluetooth adapter is turning on. However local cl ients should wait | |
| 898 // for STATE_ON before attempting to use the adapter. | |
| 899 stateString = "TURNING_ON"; | |
| 900 break; | |
| 901 default: | |
| 902 stateString = "INVALID"; | |
| 903 break; | |
| 904 } | |
| 905 return stateString; | |
| 906 } | |
| 907 | |
| 908 // Logs the state of the local Bluetooth adapter. | |
| 909 private void logBluetoothAdapterInfo(BluetoothAdapter localAdapter) { | |
| 910 Log.d(TAG_BT, "BluetoothAdapter: " | |
| 911 + "enabled=" + localAdapter.isEnabled() + ", " | |
| 912 + "state=" + stateToString(localAdapter.getState()) + ", " | |
| 913 + "name=" + localAdapter.getName() + ", " | |
| 914 + "address=" + localAdapter.getAddress()); | |
| 915 // Log the set of BluetoothDevice objects that are bonded (paired) to the lo cal adapter. | |
| 916 Set<BluetoothDevice> pairedDevices = localAdapter.getBondedDevices(); | |
| 917 if (pairedDevices.size() > 0) { | |
| 918 Log.d(TAG_BT, "paired devices:"); | |
| 919 for (BluetoothDevice device : pairedDevices) { | |
| 920 Log.d(TAG_BT, " name=" + device.getName() + ", address=" + device.getAdd ress()); | |
| 921 } | |
| 417 } | 922 } |
| 418 } | 923 } |
| 419 } | 924 } |
| OLD | NEW |