Chromium Code Reviews| Index: webrtc/examples/androidapp/src/org/appspot/apprtc/AppRTCAudioManager.java |
| diff --git a/webrtc/examples/androidapp/src/org/appspot/apprtc/AppRTCAudioManager.java b/webrtc/examples/androidapp/src/org/appspot/apprtc/AppRTCAudioManager.java |
| index 6e66b08395ca78e26a72887ce831ab6c4cf232d3..8ca0333b14d1e54c64daa29060f4865d57b5c128 100644 |
| --- a/webrtc/examples/androidapp/src/org/appspot/apprtc/AppRTCAudioManager.java |
| +++ b/webrtc/examples/androidapp/src/org/appspot/apprtc/AppRTCAudioManager.java |
| @@ -12,6 +12,10 @@ package org.appspot.apprtc; |
| import org.appspot.apprtc.util.AppRTCUtils; |
| +import android.bluetooth.BluetoothAdapter; |
| +import android.bluetooth.BluetoothDevice; |
| +import android.bluetooth.BluetoothHeadset; |
| +import android.bluetooth.BluetoothProfile; |
| import android.content.BroadcastReceiver; |
| import android.content.Context; |
| import android.content.Intent; |
| @@ -19,54 +23,107 @@ import android.content.IntentFilter; |
| import android.content.SharedPreferences; |
| import android.content.pm.PackageManager; |
| import android.media.AudioManager; |
| +import android.os.Handler; |
| +import android.os.HandlerThread; |
| +import android.os.Process; |
| import android.preference.PreferenceManager; |
| import android.util.Log; |
| import java.util.Collections; |
| import java.util.HashSet; |
| +import java.util.List; |
| import java.util.Set; |
| -/** |
| - * AppRTCAudioManager manages all audio related parts of the AppRTC demo. |
| - */ |
| +// 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.
|
| public class AppRTCAudioManager { |
| private static final String TAG = "AppRTCAudioManager"; |
| + private static final String TAG_BT = "AppRTCAudioManagerBT"; |
| 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, |
| + // Timeout interval for starting or stopping audio to a Bluetooth SCO device. |
| + private static final int BLUETOOTH_SCO_TIMEOUT_MS = 4000; |
| + // Maximum number of SCO connection attempts. |
| + private static final int MAX_SCO_CONNECTION_ATTEMPTS = 2; |
| + |
| + // AudioDevice is the names of possible audio devices that we currently |
| + // support. |
| + public enum AudioDevice { SPEAKER_PHONE, WIRED_HEADSET, EARPIECE, BLUETOOTH, NONE } |
| + |
| + // AudioManager state. |
| + public enum AudioManagerState { |
| + UNINITIALIZED, |
| + PREINITIALIZED, |
| + RUNNING, |
| + } |
| + |
| + // Bluetooth connection state. |
| + public enum BluetoothState { |
| + // Bluetooth is not available; no adapter or Bluetooth is off. |
| + UNINITIALIZED, |
| + // Bluetooth error happened when trying to start Bluetooth. |
| + ERROR, |
| + // Bluetooth SCO proxy is connected, but no connected Bluetooth headset devices, |
| + // SCO is not started or disconnected. |
| + HEADSET_UNAVAILABLE, |
| + // Bluetooth SCO proxy is connected, connected Bluetooth headset present, but SCO is |
| + // not started or disconnected. |
| + HEADSET_AVAILABLE, |
| + // Bluetooth audio SCO connection with remote device is closing. |
| + SCO_DISCONNECTING, |
| + // Bluetooth audio SCO connection with remote device is initiated. |
| + SCO_CONNECTING, |
| + // Bluetooth audio SCO connection with remote device is established. |
| + SCO_CONNECTED |
| + } |
| + |
| + // Selected audio device change event. |
| + public static interface AudioManagerEvents { |
| + // Callback fired once audio device is changed or list of available audio devices changed. |
| + void onAudioDeviceChanged( |
| + AudioDevice selectedAudioDevice, Set<AudioDevice> availableAudioDevices); |
| } |
| private final Context apprtcContext; |
| - private final Runnable onStateChangeListener; |
| - private boolean initialized = false; |
| private AudioManager audioManager; |
| + private final Handler handler; |
| + |
| + private AudioManagerEvents audioManagerEvents; |
| + private AudioManagerState amState; |
| private int savedAudioMode = AudioManager.MODE_INVALID; |
| private boolean savedIsSpeakerPhoneOn = false; |
| private boolean savedIsMicrophoneMute = false; |
| + private boolean hasWiredHeadset = false; |
| - private final AudioDevice defaultAudioDevice; |
| + // Default audio device; speaker phone for video calls or earpiece for audio |
| + // only calls. |
| + private AudioDevice defaultAudioDevice; |
| + |
| + // Contains the currently selected audio device. |
| + private AudioDevice selectedAudioDevice; |
| + |
| + // Contains user selected audio device. |
| + // TODO(henrika): always set to AudioDevice.NONE today. Add support for |
| + // explicit selection based on choice by userSelectedAudioDevice. |
| + private AudioDevice userSelectedAudioDevice; |
| // 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; |
| + // Enabled during initialization if BLUETOOTH permission is granted. |
| + 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.
|
| - // Contains the currently selected audio device. |
| - private AudioDevice selectedAudioDevice; |
| + // Listener for when we have been connected or disconnected to the Headset |
| + // Bluetooth profile. |
| + private final BluetoothProfile.ServiceListener bluetoothServiceListener; |
| + |
| + private BluetoothState bluetoothState; |
| + private BluetoothAdapter bluetoothAdapter; |
| + private BluetoothHeadset bluetoothHeadset; |
| + private BluetoothDevice bluetoothDevice; |
| + // Number of Bluetooth SCO connection attempts. |
| + int scoConnectionAttempts; |
| // Contains a list of available audio devices. A Set collection is used to |
| // avoid duplicate elements. |
| @@ -75,76 +132,198 @@ public class AppRTCAudioManager { |
| // Broadcast receiver for wired headset intent broadcasts. |
| private BroadcastReceiver wiredHeadsetReceiver; |
| + // Broadcast receiver for Bluetooth headset intent broadcasts. Detects |
| + // headset changes and Bluetooth SCO state changes. |
| + private final BroadcastReceiver bluetoothHeadsetReceiver; |
| + |
| // Callback method for changes in audio focus. |
| private AudioManager.OnAudioFocusChangeListener audioFocusChangeListener; |
| - // 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; |
| + // Runs when the Bluetooth timeout expires. We use that timeout after calling |
| + // startBluetoothSco() or stopBluetoothSco() because we're not guaranteed to |
| + // get a callback after those calls. |
| + private final Runnable bluetoothTimeoutRunnable = new Runnable() { |
| + @Override |
| + public void run() { |
| + bluetoothTimeout(); |
| + } |
| + }; |
| + |
| + // Receiver which handles changes in wired headset availability. |
| + private class WiredHeadsetReceiver extends 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, "WiredHeadsetReceiver.onReceive" + AppRTCUtils.getThreadInfo() + ": " |
| + + "a=" + intent.getAction() + ", s=" |
| + + (state == STATE_UNPLUGGED ? "unplugged" : "plugged") + ", m=" |
| + + (microphone == HAS_MIC ? "mic" : "no mic") + ", n=" + name + ", sb=" |
| + + isInitialStickyBroadcast()); |
| + hasWiredHeadset = (state == STATE_PLUGGED); |
| + updateAudioDeviceState(); |
| + } |
| + }; |
| + |
| + // Intent broadcast receiver which handles changes in Bluetooth device availability. |
| + private class BluetoothHeadsetBroadcastReceiver extends BroadcastReceiver { |
| + @Override |
| + public void onReceive(Context context, Intent intent) { |
| + if (amState != AudioManagerState.RUNNING || bluetoothState == BluetoothState.UNINITIALIZED) { |
| + return; |
| + } |
| + final String action = intent.getAction(); |
| + // Change in connection state of the Headset profile. Note that the |
| + // change does not tell us anything about whether we're streaming |
| + // audio to BT over SCO. |
| + if (action.equals(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED)) { |
| + final int state = |
| + intent.getIntExtra(BluetoothHeadset.EXTRA_STATE, BluetoothHeadset.STATE_DISCONNECTED); |
| + final BluetoothDevice device = |
| + (BluetoothDevice) intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); |
| + Log.d(TAG_BT, "BluetoothHeadsetBroadcastReceiver.onReceive: " |
| + + "a=ACTION_CONNECTION_STATE_CHANGED, " |
| + + "s=" + stateToString(state) + ", " |
| + + "n=" + device.getName() + ", " |
| + + "sb=" + isInitialStickyBroadcast() + ", " |
| + + "BT state: " + bluetoothState); |
| + if (state == BluetoothHeadset.STATE_CONNECTED) { |
| + scoConnectionAttempts = 0; |
| + updateAudioDeviceState(); |
| + } else if (state == BluetoothHeadset.STATE_CONNECTING) { |
| + // No action needed. |
| + } else if (state == BluetoothHeadset.STATE_DISCONNECTING) { |
| + // No action needed. |
| + } else if (state == BluetoothHeadset.STATE_DISCONNECTED) { |
| + // Bluetooth is probably powered off during the call. |
| + stopBluetoothSco(); |
| + updateAudioDeviceState(); |
| + } |
| + // Change in the audio (SCO) connection state of the Headset profile. |
| + } else if (action.equals(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED)) { |
| + final int state = intent.getIntExtra( |
| + BluetoothHeadset.EXTRA_STATE, BluetoothHeadset.STATE_AUDIO_DISCONNECTED); |
| + final BluetoothDevice device = |
| + (BluetoothDevice) intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); |
| + Log.d(TAG_BT, "BluetoothHeadsetBroadcastReceiver.onReceive: " |
| + + "a=ACTION_AUDIO_STATE_CHANGED, " |
| + + "s=" + stateToString(state) + ", " |
| + + "n=" + device.getName() + ", " |
| + + "sb=" + isInitialStickyBroadcast() + ", " |
| + + "BT state: " + bluetoothState); |
| + if (state == BluetoothHeadset.STATE_AUDIO_CONNECTED) { |
| + cancelBluetoothTimer(); |
| + if (bluetoothState == BluetoothState.SCO_CONNECTING) { |
| + bluetoothState = BluetoothState.SCO_CONNECTED; |
| + scoConnectionAttempts = 0; |
| + updateAudioDeviceState(); |
| + } else { |
| + Log.w(TAG_BT, "Unexpected state BluetoothHeadset.STATE_AUDIO_CONNECTED"); |
| + } |
| + } else if (state == BluetoothHeadset.STATE_AUDIO_CONNECTING) { |
| + // No action needed. |
| + } else if (state == BluetoothHeadset.STATE_AUDIO_DISCONNECTED) { |
| + if (isInitialStickyBroadcast()) { |
| + Log.d(TAG_BT, "Ignore STATE_AUDIO_DISCONNECTED initial sticky broadcast."); |
| + return; |
| + } |
| + stopBluetoothSco(); |
| + updateAudioDeviceState(); |
| + } |
| + } |
| + } |
| + }; |
| + |
| + // Implementation of an interface that notifies BluetoothProfile IPC clients when they have been |
| + // connected to or disconnected from the service. |
| + private class BluetoothServiceListener implements BluetoothProfile.ServiceListener { |
| + @Override |
| + // Called to notify the client when the proxy object has been connected to the service. |
| + public void onServiceConnected(int profile, BluetoothProfile proxy) { |
| + if (profile != BluetoothProfile.HEADSET || bluetoothState == BluetoothState.UNINITIALIZED) { |
| + return; |
| + } |
| + Log.d(TAG_BT, "BluetoothServiceListener.onServiceConnected: BT state=" + bluetoothState); |
| + // Android only supports one connected Bluetooth Headset at a time. |
| + bluetoothHeadset = (BluetoothHeadset) proxy; |
| + // Update BT state and list of available audio devices. |
| + updateAudioDeviceState(); |
| } |
| - // 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); |
| + @Override |
| + // Called to notify the client that this proxy object has been disconnected from the service. |
| + public void onServiceDisconnected(int profile) { |
| + if (profile != BluetoothProfile.HEADSET || bluetoothState == BluetoothState.UNINITIALIZED) { |
| + return; |
| } |
| + Log.d(TAG_BT, "BluetoothServiceListener.onServiceDisconnected: BT state=" + bluetoothState); |
| + // stopBluetoothSco(); |
| + bluetoothHeadset = null; |
| + // bluetoothDevice = null; |
| + bluetoothState = BluetoothState.HEADSET_UNAVAILABLE; |
| + // Update BT state and list of available audio devices. |
| + updateAudioDeviceState(); |
| } |
| } |
| - /** Construction */ |
| - static AppRTCAudioManager create(Context context, Runnable deviceStateChangeListener) { |
| - return new AppRTCAudioManager(context, deviceStateChangeListener); |
| + // Construction. |
| + static AppRTCAudioManager create(Context context) { |
| + return new AppRTCAudioManager(context); |
| } |
| - private AppRTCAudioManager(Context context, Runnable deviceStateChangeListener) { |
| + private AppRTCAudioManager(Context context) { |
| + Log.d(TAG, "ctor"); |
| apprtcContext = context; |
| - onStateChangeListener = deviceStateChangeListener; |
| audioManager = ((AudioManager) context.getSystemService(Context.AUDIO_SERVICE)); |
| + wiredHeadsetReceiver = new WiredHeadsetReceiver(); |
| + bluetoothServiceListener = new BluetoothServiceListener(); |
| + bluetoothHeadsetReceiver = new BluetoothHeadsetBroadcastReceiver(); |
| + amState = AudioManagerState.UNINITIALIZED; |
| + |
| + final HandlerThread handlerThread = new HandlerThread(TAG); |
| + 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
|
| + handler = new Handler(handlerThread.getLooper()); |
| SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); |
| useSpeakerphone = sharedPreferences.getString(context.getString(R.string.pref_speakerphone_key), |
| context.getString(R.string.pref_speakerphone_default)); |
| - |
| + Log.d(TAG, "useSpeakerphone: " + useSpeakerphone); |
| 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(); |
| - } |
| - }); |
| + Log.d(TAG, "defaultAudioDevice: " + defaultAudioDevice); |
| AppRTCUtils.logDeviceInfo(TAG); |
| + |
| + // Check if this process has the BLUETOOTH permission or not. |
| + hasBluetoothPermission = hasPermission(android.Manifest.permission.BLUETOOTH); |
| + Log.d(TAG_BT, "hasBluetoothPermission: " + hasBluetoothPermission); |
| } |
| - public void init() { |
| - Log.d(TAG, "init"); |
| - if (initialized) { |
| + public void start(AudioManagerEvents audioManagerEvents) { |
| + Log.d(TAG, "start"); |
| + if (amState == AudioManagerState.RUNNING) { |
| + Log.e(TAG, "AudioManager is already active"); |
| return; |
| } |
| + // TODO(henrika): perhaps call new method called preInitAudio() here if UNINITIALIZED. |
| + |
| + Log.d(TAG, "AudioManager starts..."); |
| + this.audioManagerEvents = audioManagerEvents; |
| + amState = AudioManagerState.RUNNING; |
| - // Store current audio state so we can restore it when close() is called. |
| + // Store current audio state so we can restore it when stop() is called. |
| savedAudioMode = audioManager.getMode(); |
| savedIsSpeakerPhoneOn = audioManager.isSpeakerphoneOn(); |
| savedIsMicrophoneMute = audioManager.isMicrophoneMute(); |
| + hasWiredHeadset = hasWiredHeadset(); |
| // Create an AudioManager.OnAudioFocusChangeListener instance. |
| audioFocusChangeListener = new AudioManager.OnAudioFocusChangeListener() { |
| @@ -199,31 +378,51 @@ public class AppRTCAudioManager { |
| // 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); |
| + // Set initial device states. |
| + userSelectedAudioDevice = AudioDevice.NONE; |
| + selectedAudioDevice = AudioDevice.NONE; |
| + audioDevices.clear(); |
| + |
| + // Initialize and start Bluetooth if a BT device is available or initiate |
| + // detection of new (enabled) BT devices. |
| + bluetoothState = BluetoothState.UNINITIALIZED; |
| + startBluetooth(); |
| + |
| // 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()); |
| + // either by adding/removing a BT or wired headset or by covering/uncovering |
| + // the proximity sensor. |
| + updateAudioDeviceState(); |
| // Register receiver for broadcast intents related to adding/removing a |
| - // wired headset (Intent.ACTION_HEADSET_PLUG). |
| - registerForWiredHeadsetIntentBroadcast(); |
| - |
| - initialized = true; |
| + // wired headset. |
| + registerReceiver(wiredHeadsetReceiver, new IntentFilter(Intent.ACTION_HEADSET_PLUG)); |
| + |
| + IntentFilter bluetoothHeadsetFilter = new IntentFilter(); |
| + // Register receiver for change in connection state of the Headset profile. |
| + bluetoothHeadsetFilter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED); |
| + // Register receiver for change in audio connection state of the Headset profile. |
| + bluetoothHeadsetFilter.addAction(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED); |
| + registerReceiver(bluetoothHeadsetReceiver, bluetoothHeadsetFilter); |
| + Log.d(TAG, "AudioManager started"); |
| } |
| - public void close() { |
| - Log.d(TAG, "close"); |
| - if (!initialized) { |
| + public void stop() { |
| + Log.d(TAG, "stop"); |
| + if (amState != AudioManagerState.RUNNING) { |
| + Log.e(TAG, "Trying to stop AudioManager in incorrect state: " + amState); |
| return; |
| } |
| + amState = AudioManagerState.UNINITIALIZED; |
| - unregisterForWiredHeadsetIntentBroadcast(); |
| + // Cancel Bluetooth related tasks. |
| + unregisterReceiver(bluetoothHeadsetReceiver); |
| + unregisterReceiver(wiredHeadsetReceiver); |
| + stopBluetooth(); |
| // Restore previously stored audio states. |
| setSpeakerphoneOn(savedIsSpeakerPhoneOn); |
| @@ -235,102 +434,87 @@ public class AppRTCAudioManager { |
| audioFocusChangeListener = null; |
| Log.d(TAG, "Abandoned audio focus for VOICE_CALL streams"); |
| - if (proximitySensor != null) { |
| - proximitySensor.stop(); |
| - proximitySensor = null; |
| - } |
| - |
| - initialized = false; |
| + audioManagerEvents = null; |
| + Log.d(TAG, "AudioManager stopped"); |
| } |
| - /** Changes selection of the currently active audio device. */ |
| - public void setAudioDevice(AudioDevice device) { |
| - Log.d(TAG, "setAudioDevice(device=" + device + ")"); |
| + // Changes selection of the currently active audio device. |
| + private void setAudioDeviceInternal(AudioDevice device) { |
| + Log.d(TAG, "setAudioDeviceInternal(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; |
| + case BLUETOOTH: |
| + setSpeakerphoneOn(false); |
| break; |
| default: |
| Log.e(TAG, "Invalid audio device selection"); |
| break; |
| } |
| - onAudioManagerChangedState(); |
| + selectedAudioDevice = device; |
| } |
| - /** Returns current set of available/selectable audio devices. */ |
| - public Set<AudioDevice> getAudioDevices() { |
| - return Collections.unmodifiableSet(new HashSet<AudioDevice>(audioDevices)); |
| + // Changes default audio device. |
| + // TODO(henrika): add usage of this method in the AppRTCMobile client. |
| + public synchronized void setDefaultAudioDevice(AudioDevice defaultDevice) { |
| + switch (defaultDevice) { |
| + case SPEAKER_PHONE: |
| + defaultAudioDevice = defaultDevice; |
| + break; |
| + case EARPIECE: |
| + if (hasEarpiece()) { |
| + defaultAudioDevice = defaultDevice; |
| + } else { |
| + defaultAudioDevice = AudioDevice.SPEAKER_PHONE; |
| + } |
| + break; |
| + default: |
| + Log.e(TAG, "Invalid default audio device selection"); |
| + break; |
| + } |
| + Log.d(TAG, "setDefaultAudioDevice(device=" + defaultAudioDevice + ")"); |
| + updateAudioDeviceState(); |
| } |
| - /** Returns the currently selected audio device. */ |
| - public AudioDevice getSelectedAudioDevice() { |
| - return selectedAudioDevice; |
| + // Changes selection of the currently active audio device. |
| + public synchronized void selectAudioDevice(AudioDevice device) { |
| + if (!audioDevices.contains(device)) { |
| + Log.e(TAG, "Can not select " + device + " from available " + audioDevices); |
| + } |
| + userSelectedAudioDevice = device; |
| + updateAudioDeviceState(); |
| } |
| - /** |
| - * 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; |
| + // Returns current set of available/selectable audio devices. |
| + public synchronized Set<AudioDevice> getAudioDevices() { |
| + return Collections.unmodifiableSet(new HashSet<AudioDevice>(audioDevices)); |
| + } |
| - @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; |
| - } |
| - } |
| - }; |
| + // Returns the currently selected audio device. |
| + public synchronized AudioDevice getSelectedAudioDevice() { |
| + return selectedAudioDevice; |
| + } |
| - apprtcContext.registerReceiver(wiredHeadsetReceiver, filter); |
| + // Helper method for receiver registration. |
| + private void registerReceiver(BroadcastReceiver receiver, IntentFilter filter) { |
| + apprtcContext.registerReceiver(receiver, filter); |
| } |
| - /** Unregister receiver for broadcasted ACTION_HEADSET_PLUG intent. */ |
| - private void unregisterForWiredHeadsetIntentBroadcast() { |
| - apprtcContext.unregisterReceiver(wiredHeadsetReceiver); |
| - wiredHeadsetReceiver = null; |
| + // Helper method for unregistration of an existing receiver. |
| + private void unregisterReceiver(BroadcastReceiver receiver) { |
| + apprtcContext.unregisterReceiver(receiver); |
| } |
| - /** Sets the speaker phone mode. */ |
| + // Sets the speaker phone mode. |
| private void setSpeakerphoneOn(boolean on) { |
| boolean wasOn = audioManager.isSpeakerphoneOn(); |
| if (wasOn == on) { |
| @@ -339,7 +523,7 @@ public class AppRTCAudioManager { |
| audioManager.setSpeakerphoneOn(on); |
| } |
| - /** Sets the microphone mute state. */ |
| + // Sets the microphone mute state. |
| private void setMicrophoneMute(boolean on) { |
| boolean wasMuted = audioManager.isMicrophoneMute(); |
| if (wasMuted == on) { |
| @@ -348,72 +532,393 @@ public class AppRTCAudioManager { |
| audioManager.setMicrophoneMute(on); |
| } |
| - /** Gets the current earpiece state. */ |
| + // Starts all Bluetooth related tasks. |
| + void startBluetooth() { |
| + Log.d(TAG_BT, "startBluetooth"); |
| + if (!hasBluetoothPermission) { |
| + Log.w(TAG_BT, "Process lacks BLUETOOTH permission"); |
| + return; |
| + } |
| + if (bluetoothState != BluetoothState.UNINITIALIZED) { |
| + Log.w(TAG_BT, "Invalid BT state"); |
| + return; |
| + } |
| + bluetoothHeadset = null; |
| + bluetoothDevice = null; |
| + scoConnectionAttempts = 0; |
| + // Get a handle to the default local Bluetooth adapter. |
| + bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); |
| + if (bluetoothAdapter == null) { |
| + Log.w(TAG_BT, "Device does not support Bluetooth"); |
| + return; |
| + } |
| + // Ensure that the device supports use of BT SCO audio for off call use cases. |
| + if (!audioManager.isBluetoothScoAvailableOffCall()) { |
| + Log.e(TAG_BT, "Bluetooth SCO audio is not available off call"); |
| + return; |
| + } |
| + logBluetoothAdapterInfo(bluetoothAdapter); |
| + // Establish a connection to the HEADSET profile (includes both Bluetooth Headset and |
| + // Hands-Free) proxy object and install a listener. |
| + if (!bluetoothAdapter.getProfileProxy( |
| + apprtcContext, bluetoothServiceListener, BluetoothProfile.HEADSET)) { |
| + Log.e(TAG_BT, "BluetoothAdapter.getProfileProxy(HEADSET) failed"); |
| + return; |
| + } |
| + Log.d(TAG_BT, "HEADSET profile state: " |
| + + stateToString(bluetoothAdapter.getProfileConnectionState(BluetoothProfile.HEADSET))); |
| + Log.d(TAG_BT, "Bluetooth proxy for headset profile has started"); |
| + bluetoothState = BluetoothState.HEADSET_UNAVAILABLE; |
| + } |
| + |
| + // Stops all Bluetooth tasks. |
| + void stopBluetooth() { |
| + Log.d(TAG_BT, "stopBluetooth: BT state=" + bluetoothState); |
| + if (bluetoothAdapter != null) { |
| + // Stop BT SCO connection with remote device if needed. |
| + stopBluetoothSco(); |
| + // Close down remaining BT resources. |
| + if (bluetoothState != BluetoothState.UNINITIALIZED) { |
| + cancelBluetoothTimer(); |
| + if (bluetoothHeadset != null) { |
| + bluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, bluetoothHeadset); |
| + bluetoothHeadset = null; |
| + } |
| + bluetoothAdapter = null; |
| + bluetoothDevice = null; |
| + bluetoothState = BluetoothState.UNINITIALIZED; |
| + } |
| + } |
| + } |
| + |
| + // Called when start of the BT SCO channel takes too long time. Usually |
| + // happens when the BT device has been turned on during an ongoing call. |
| + private void bluetoothTimeout() { |
| + if (amState != AudioManagerState.RUNNING || bluetoothState == BluetoothState.UNINITIALIZED |
| + || bluetoothHeadset == null) { |
| + return; |
| + } |
| + Log.d(TAG_BT, "bluetoothTimeout: BT state=" + bluetoothState); |
| + if (bluetoothState != BluetoothState.SCO_CONNECTING) { |
| + return; |
| + } |
| + // Bluetooth SCO should be connecting; check the latest result. |
| + boolean scoConnected = false; |
| + List<BluetoothDevice> devices = bluetoothHeadset.getConnectedDevices(); |
| + if (devices.size() > 0) { |
| + bluetoothDevice = devices.get(0); |
| + if (bluetoothHeadset.isAudioConnected(bluetoothDevice)) { |
| + Log.d(TAG_BT, "SCO connected with " + bluetoothDevice.getName()); |
| + scoConnected = true; |
| + } else { |
| + Log.d(TAG_BT, "SCO is not connected with " + bluetoothDevice.getName()); |
| + } |
| + } |
| + |
| + if (scoConnected) { |
| + // We thought BT had timed out, but it's actually on; updating state. |
| + bluetoothState = BluetoothState.SCO_CONNECTED; |
| + scoConnectionAttempts = 0; |
| + updateAudioDeviceState(); |
| + } else { |
| + // Give up and "cancel" our request by calling stopBluetoothSco(). |
| + Log.w(TAG_BT, "BT failed to connect after timeout"); |
| + stopBluetoothSco(); |
| + updateAudioDeviceState(); |
| + } |
| + } |
| + |
| + private void startBluetoothTimer() { |
| + Log.d(TAG_BT, "startBluetoothTimer"); |
| + handler.postDelayed(bluetoothTimeoutRunnable, BLUETOOTH_SCO_TIMEOUT_MS); |
| + } |
| + |
| + private void cancelBluetoothTimer() { |
| + Log.d(TAG_BT, "cancelBluetoothTimer"); |
| + handler.removeCallbacks(bluetoothTimeoutRunnable); |
| + } |
| + |
| + // Starts Bluetooth SCO connection with remote device. |
| + // Note that the phone application always has the priority on the usage of the SCO connection |
| + // for telephony. If this method is called while the phone is in call it will be ignored. |
| + // Similarly, if a call is received or sent while an application is using the SCO connection, |
| + // the connection will be lost for the application and NOT returned automatically when the call |
| + // ends. Also note that: up to and including API version JELLY_BEAN_MR1, this method initiates a |
| + // virtual voice call to the Bluetooth headset. After API version JELLY_BEAN_MR2 only a raw SCO |
| + // audio connection is established. |
| + // TODO(henrika): should we add support for virtual voice call to BT headset also for JBMR2 and |
| + // higher. It might be required to initiates a virtual voice call since many devices do not |
| + // accept SCO audio without a "call". |
| + private void startBluetoothSco() { |
| + if (bluetoothState != BluetoothState.HEADSET_AVAILABLE) { |
| + return; |
| + } |
| + Log.d(TAG_BT, "startBluetoothSco: BT state=" + bluetoothState + ", " |
| + + "attempt: " + scoConnectionAttempts + ", " |
| + + "SCO is on: " + audioManager.isBluetoothScoOn()); |
| + // The SCO connection establishment can take several seconds, hence we cannot rely on the |
| + // connection to be available when the method returns but instead register to receive the |
| + // intent ACTION_SCO_AUDIO_STATE_UPDATED and wait for the state to be SCO_AUDIO_STATE_CONNECTED. |
| + bluetoothState = BluetoothState.SCO_CONNECTING; |
| + audioManager.startBluetoothSco(); |
| + scoConnectionAttempts++; |
| + startBluetoothTimer(); |
| + } |
| + |
| + // Stops Bluetooth SCO connection with remote device. |
| + private void stopBluetoothSco() { |
| + if (bluetoothState != BluetoothState.SCO_CONNECTING |
| + && bluetoothState != BluetoothState.SCO_CONNECTED) { |
| + return; |
| + } |
| + Log.d(TAG_BT, "stopBluetoothSco: BT state=" + bluetoothState + ", " |
| + + "SCO is on: " + audioManager.isBluetoothScoOn()); |
| + cancelBluetoothTimer(); |
| + audioManager.stopBluetoothSco(); |
| + bluetoothState = BluetoothState.SCO_DISCONNECTING; |
| + } |
| + |
| + // Use the BluetoothHeadset proxy object (controls the Bluetooth Headset Service via IPC) to |
| + // update the list of connected devices for the HEADSET profile. |
| + private void updateBluetoothDevices() { |
| + if (amState != AudioManagerState.RUNNING || bluetoothState == BluetoothState.UNINITIALIZED |
| + || bluetoothHeadset == null) { |
| + return; |
| + } |
| + Log.d(TAG_BT, "updateBluetoothDevices"); |
| + // Update the Bluetooth device list for the headset profile. |
| + // The BluetoothDevice class is just a thin wrapper for a Bluetooth hardware address. |
| + List<BluetoothDevice> devices = bluetoothHeadset.getConnectedDevices(); |
| + if (devices.size() == 0) { |
| + bluetoothDevice = null; |
| + } else { |
| + bluetoothDevice = devices.get(0); |
| + } |
| + if (bluetoothDevice == null) { |
| + bluetoothState = BluetoothState.HEADSET_UNAVAILABLE; |
| + Log.d(TAG_BT, "No connected bluetooth headset"); |
| + } else { |
| + bluetoothState = BluetoothState.HEADSET_AVAILABLE; |
| + Log.d(TAG_BT, "Connected bluetooth headset: " |
| + + "name=" + bluetoothDevice.getName() + ", " |
| + + "state=" + stateToString(bluetoothHeadset.getConnectionState(bluetoothDevice)) |
| + + ", SCO audio=" + bluetoothHeadset.isAudioConnected(bluetoothDevice)); |
| + } |
| + Log.d(TAG_BT, "Updated Bluetooth state: " + bluetoothState); |
| + } |
| + |
| + // 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. |
| - */ |
| + // 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(); |
| + // Update list of possible audio devices and make new device selection. |
| + private void updateAudioDeviceState() { |
| + Log.d(TAG, "--- updateAudioDeviceState: " |
| + + "wired headset=" + hasWiredHeadset + ", " |
| + + "BT state=" + bluetoothState); |
| + Log.d(TAG, "Device status: " |
| + + "available=" + audioDevices + ", " |
| + + "selected=" + selectedAudioDevice + ", " |
| + + "user selected=" + userSelectedAudioDevice); |
| + |
| + // Update Bluetooth device list. |
| + if (bluetoothState == BluetoothState.HEADSET_AVAILABLE |
| + || bluetoothState == BluetoothState.HEADSET_UNAVAILABLE |
| + || bluetoothState == BluetoothState.SCO_DISCONNECTING) { |
| + updateBluetoothDevices(); |
| + } |
| + |
| + // Update the set of available audio devices. |
| + Set<AudioDevice> newAudioDevices = new HashSet<>(); |
| + |
| + if (bluetoothState == BluetoothState.SCO_CONNECTED |
| + || bluetoothState == BluetoothState.SCO_CONNECTING |
| + || bluetoothState == BluetoothState.HEADSET_AVAILABLE) { |
| + newAudioDevices.add(AudioDevice.BLUETOOTH); |
| + } |
| if (hasWiredHeadset) { |
| // If a wired headset is connected, then it is the only possible option. |
| - audioDevices.add(AudioDevice.WIRED_HEADSET); |
| + newAudioDevices.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); |
| + newAudioDevices.add(AudioDevice.SPEAKER_PHONE); |
| if (hasEarpiece()) { |
| - audioDevices.add(AudioDevice.EARPIECE); |
| + newAudioDevices.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); |
| + // Update the existing audio device set if needed. |
| + boolean audioDeviceSetUpdated = |
| + !audioDevices.containsAll(newAudioDevices) || !newAudioDevices.containsAll(audioDevices); |
| + if (audioDeviceSetUpdated) { |
| + audioDevices.clear(); |
| + audioDevices.addAll(newAudioDevices); |
| + } |
| + |
| + // Correct user selected audio devices if needed. |
| + if (bluetoothState == BluetoothState.HEADSET_UNAVAILABLE |
| + && userSelectedAudioDevice == AudioDevice.BLUETOOTH) { |
| + // If BT is not available, it can't be the user selection. |
| + userSelectedAudioDevice = AudioDevice.NONE; |
| + } |
| + if (hasWiredHeadset && userSelectedAudioDevice == AudioDevice.SPEAKER_PHONE) { |
| + // If user selected speaker phone, but then plugged wired headset then make |
| + // wired headset as user selected device. |
| + userSelectedAudioDevice = AudioDevice.WIRED_HEADSET; |
| + } |
| + if (!hasWiredHeadset && userSelectedAudioDevice == AudioDevice.WIRED_HEADSET) { |
| + // If user selected wired headset, but then unplugged wired headset then make |
| + // speaker phone as user selected device. |
| + userSelectedAudioDevice = AudioDevice.SPEAKER_PHONE; |
| + } |
| + |
| + // Need to start Bluetooth if it is available and user either selected it explicitly or |
| + // user did not select any output device. |
| + boolean needBluetoothStart = bluetoothState == BluetoothState.HEADSET_AVAILABLE |
| + && (userSelectedAudioDevice == AudioDevice.NONE |
| + || userSelectedAudioDevice == AudioDevice.BLUETOOTH); |
| + |
| + // Need to stop Bluetooth if user selected different device and Bluetooth SCO connection is |
| + // established or in the process. |
| + boolean needBluetoothStop = (bluetoothState == BluetoothState.SCO_CONNECTED |
| + || bluetoothState == BluetoothState.SCO_CONNECTING) |
| + && (userSelectedAudioDevice != AudioDevice.NONE |
| + && userSelectedAudioDevice != AudioDevice.BLUETOOTH); |
| + |
| + if (bluetoothState == BluetoothState.HEADSET_AVAILABLE |
| + || bluetoothState == BluetoothState.SCO_CONNECTING |
| + || bluetoothState == BluetoothState.SCO_CONNECTED) { |
| + Log.d(TAG_BT, "Need BT: start=" + needBluetoothStart + ", " |
| + + "stop=" + needBluetoothStop + ", " |
| + + "BT state=" + bluetoothState); |
| + } |
| + |
| + // Start/stop Bluetooth SCO connection |
| + if (needBluetoothStop) { |
| + stopBluetoothSco(); |
| + updateBluetoothDevices(); |
| + } |
| + |
| + if (needBluetoothStart && !needBluetoothStop) { |
| + if (scoConnectionAttempts < MAX_SCO_CONNECTION_ATTEMPTS) { |
| + // Starts BT SCO channel and waits for ACTION_AUDIO_STATE_CHANGED. |
| + Log.d(TAG_BT, "Starting Bluetooth SCO and waits for ACTION_AUDIO_STATE_CHANGED..."); |
| + startBluetoothSco(); |
| + } else { |
| + Log.e(TAG_BT, "BT SCO connection fails - no more attempts"); |
| + audioDevices.remove(AudioDevice.BLUETOOTH); |
| + audioDeviceSetUpdated = true; |
| + } |
| + } |
| + |
| + // Update selected audio device. |
| + AudioDevice newAudioDevice = selectedAudioDevice; |
| + |
| + if (bluetoothState == BluetoothState.SCO_CONNECTED) { |
| + // If a Bluetooth is connected, then it should be used as output audio device. |
| + newAudioDevice = AudioDevice.BLUETOOTH; |
| + } else if (hasWiredHeadset) { |
| + // If a wired headset is connected, but Bluetooth is not, then wired headset is used as |
| + // output device. |
| + newAudioDevice = AudioDevice.WIRED_HEADSET; |
| } else { |
| - setAudioDevice(defaultAudioDevice); |
| + // No wired headset and no Bluetooth, hence the audio-device list can contain speaker |
| + // phone (on a tablet), or speaker phone and earpiece (on mobile phone). |
| + // |defaultAudioDevice| contains either AudioDevice.SPEAKER_PHONE or AudioDevice.EARPIECE |
| + // depending on the user's selection. |
| + newAudioDevice = defaultAudioDevice; |
| + } |
| + // Switch to new device but only if there has been any changes. |
| + if (newAudioDevice != selectedAudioDevice || audioDeviceSetUpdated) { |
| + // Do the required device switch. |
| + setAudioDeviceInternal(newAudioDevice); |
| + Log.d(TAG, "New device status: " |
| + + "available=" + audioDevices + ", " |
| + + "selected=" + newAudioDevice); |
| + if (audioManagerEvents != null) { |
| + // Notify a listening client that audio device has been changed. |
| + audioManagerEvents.onAudioDeviceChanged(selectedAudioDevice, audioDevices); |
| + } |
| } |
| + Log.d(TAG, "--- updateAudioDeviceState done"); |
| } |
| - /** 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"); |
| + // Checks if the process has as specified permission or not. |
| + private boolean hasPermission(String permission) { |
| + return apprtcContext.checkPermission(permission, Process.myPid(), Process.myUid()) |
| + == PackageManager.PERMISSION_GRANTED; |
| + } |
| + |
| + private String stateToString(int state) { |
| + String stateString; |
| + switch (state) { |
| + case BluetoothAdapter.STATE_DISCONNECTED: |
| + // The profile is in disconnected state. |
| + stateString = "DISCONNECTED"; |
| + break; |
| + case BluetoothAdapter.STATE_CONNECTED: |
| + // The profile is in connected state. |
| + stateString = "CONNECTED"; |
| + break; |
| + case BluetoothAdapter.STATE_CONNECTING: |
| + // The profile is in connecting state. |
| + stateString = "CONNECTING"; |
| + break; |
| + case BluetoothAdapter.STATE_DISCONNECTING: |
| + // The profile is in disconnecting state. |
| + stateString = "DISCONNECTING"; |
| + break; |
| + case BluetoothAdapter.STATE_OFF: |
| + // Indicates the local Bluetooth adapter is off. |
| + stateString = "OFF"; |
| + break; |
| + case BluetoothAdapter.STATE_ON: |
| + // Indicates the local Bluetooth adapter is on, and ready for use. |
| + stateString = "ON"; |
| + break; |
| + case BluetoothAdapter.STATE_TURNING_OFF: |
| + // Indicates the local Bluetooth adapter is turning off. Local clients should immediately |
| + // attempt graceful disconnection of any remote links. |
| + stateString = "TURNING_OFF"; |
| + break; |
| + case BluetoothAdapter.STATE_TURNING_ON: |
| + // Indicates the local Bluetooth adapter is turning on. However local clients should wait |
| + // for STATE_ON before attempting to use the adapter. |
| + stateString = "TURNING_ON"; |
| + break; |
| + default: |
| + stateString = "INVALID"; |
| + break; |
| } |
| + return stateString; |
| + } |
| - if (onStateChangeListener != null) { |
| - // Run callback to notify a listening client. The client can then |
| - // use public getters to query the new state. |
| - onStateChangeListener.run(); |
| + // Logs the state of the local Bluetooth adapter. |
| + private void logBluetoothAdapterInfo(BluetoothAdapter localAdapter) { |
| + Log.d(TAG_BT, "BluetoothAdapter: " |
| + + "enabled=" + localAdapter.isEnabled() + ", " |
| + + "state=" + stateToString(localAdapter.getState()) + ", " |
| + + "name=" + localAdapter.getName() + ", " |
| + + "address=" + localAdapter.getAddress()); |
| + // Log the set of BluetoothDevice objects that are bonded (paired) to the local adapter. |
| + Set<BluetoothDevice> pairedDevices = localAdapter.getBondedDevices(); |
| + if (pairedDevices.size() > 0) { |
| + Log.d(TAG_BT, "paired devices:"); |
| + for (BluetoothDevice device : pairedDevices) { |
| + Log.d(TAG_BT, " name=" + device.getName() + ", address=" + device.getAddress()); |
| + } |
| } |
| } |
| } |