add permission checks to appRTCBluetoothManager

Daniel Gultsch created

Change summary

src/main/AndroidManifest.xml                                              |   1 
src/main/java/eu/siacs/conversations/services/AppRTCBluetoothManager.java | 317 
2 files changed, 172 insertions(+), 146 deletions(-)

Detailed changes

src/main/AndroidManifest.xml 🔗

@@ -2,6 +2,7 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:tools="http://schemas.android.com/tools">
 
+    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
     <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
     <uses-permission android:name="android.permission.READ_CONTACTS" />

src/main/java/eu/siacs/conversations/services/AppRTCBluetoothManager.java 🔗

@@ -9,6 +9,7 @@
  */
 package eu.siacs.conversations.services;
 
+import android.Manifest;
 import android.annotation.SuppressLint;
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothDevice;
@@ -20,25 +21,25 @@ import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.pm.PackageManager;
 import android.media.AudioManager;
+import android.os.Build;
 import android.os.Handler;
 import android.os.Looper;
-import android.os.Process;
 import android.util.Log;
 
 import androidx.annotation.Nullable;
+import androidx.core.app.ActivityCompat;
+
+import com.google.common.collect.ImmutableList;
 
 import org.webrtc.ThreadUtils;
 
+import java.util.Collections;
 import java.util.List;
-import java.util.Set;
 
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.utils.AppRTCUtils;
 
-/**
- * AppRTCProximitySensor manages functions related to Bluetoth devices in the
- * AppRTC demo.
- */
+/** AppRTCProximitySensor manages functions related to Bluetoth devices in the AppRTC demo. */
 public class AppRTCBluetoothManager {
     // Timeout interval for starting or stopping audio to a Bluetooth SCO device.
     private static final int BLUETOOTH_SCO_TIMEOUT_MS = 4000;
@@ -46,28 +47,26 @@ public class AppRTCBluetoothManager {
     private static final int MAX_SCO_CONNECTION_ATTEMPTS = 2;
     private final Context apprtcContext;
     private final AppRTCAudioManager apprtcAudioManager;
-    @Nullable
-    private final AudioManager audioManager;
+    @Nullable private final AudioManager audioManager;
     private final Handler handler;
     private final BluetoothProfile.ServiceListener bluetoothServiceListener;
     private final BroadcastReceiver bluetoothHeadsetReceiver;
     int scoConnectionAttempts;
     private State bluetoothState;
-    @Nullable
-    private BluetoothAdapter bluetoothAdapter;
-    @Nullable
-    private BluetoothHeadset bluetoothHeadset;
-    @Nullable
-    private BluetoothDevice bluetoothDevice;
+    @Nullable private BluetoothAdapter bluetoothAdapter;
+    @Nullable private BluetoothHeadset bluetoothHeadset;
+    @Nullable private BluetoothDevice bluetoothDevice;
     // Runs when the Bluetooth timeout expires. We use that timeout after calling
     // startScoAudio() or stopScoAudio() because we're not guaranteed to get a
     // callback after those calls.
-    private final Runnable bluetoothTimeoutRunnable = new Runnable() {
-        @Override
-        public void run() {
-            bluetoothTimeout();
-        }
-    };
+    private final Runnable bluetoothTimeoutRunnable =
+            new Runnable() {
+                @Override
+                public void run() {
+                    bluetoothTimeout();
+                }
+            };
+
     protected AppRTCBluetoothManager(Context context, AppRTCAudioManager audioManager) {
         Log.d(Config.LOGTAG, "ctor");
         ThreadUtils.checkIsOnMainThread();
@@ -80,42 +79,29 @@ public class AppRTCBluetoothManager {
         handler = new Handler(Looper.getMainLooper());
     }
 
-    /**
-     * Construction.
-     */
+    /** Construction. */
     static AppRTCBluetoothManager create(Context context, AppRTCAudioManager audioManager) {
         Log.d(Config.LOGTAG, "create" + AppRTCUtils.getThreadInfo());
         return new AppRTCBluetoothManager(context, audioManager);
     }
 
-    /**
-     * Returns the internal state.
-     */
+    /** Returns the internal state. */
     public State getState() {
         ThreadUtils.checkIsOnMainThread();
         return bluetoothState;
     }
 
     /**
-     * Activates components required to detect Bluetooth devices and to enable
-     * BT SCO (audio is routed via BT SCO) for the headset profile. The end
-     * state will be HEADSET_UNAVAILABLE but a state machine has started which
-     * will start a state change sequence where the final outcome depends on
-     * if/when the BT headset is enabled.
-     * Example of state change sequence when start() is called while BT device
-     * is connected and enabled:
-     * UNINITIALIZED --> HEADSET_UNAVAILABLE --> HEADSET_AVAILABLE -->
-     * SCO_CONNECTING --> SCO_CONNECTED <==> audio is now routed via BT SCO.
-     * Note that the AppRTCAudioManager is also involved in driving this state
-     * change.
+     * Activates components required to detect Bluetooth devices and to enable BT SCO (audio is
+     * routed via BT SCO) for the headset profile. The end state will be HEADSET_UNAVAILABLE but a
+     * state machine has started which will start a state change sequence where the final outcome
+     * depends on if/when the BT headset is enabled. Example of state change sequence when start()
+     * is called while BT device is connected and enabled: UNINITIALIZED --> HEADSET_UNAVAILABLE -->
+     * HEADSET_AVAILABLE --> SCO_CONNECTING --> SCO_CONNECTED <==> audio is now routed via BT SCO.
+     * Note that the AppRTCAudioManager is also involved in driving this state change.
      */
     public void start() {
         ThreadUtils.checkIsOnMainThread();
-        Log.d(Config.LOGTAG, "start");
-        if (!hasPermission(apprtcContext, android.Manifest.permission.BLUETOOTH)) {
-            Log.w(Config.LOGTAG, "Process (pid=" + Process.myPid() + ") lacks BLUETOOTH permission");
-            return;
-        }
         if (bluetoothState != State.UNINITIALIZED) {
             Log.w(Config.LOGTAG, "Invalid BT state");
             return;
@@ -130,11 +116,10 @@ public class AppRTCBluetoothManager {
             return;
         }
         // Ensure that the device supports use of BT SCO audio for off call use cases.
-        if (!audioManager.isBluetoothScoAvailableOffCall()) {
+        if (this.audioManager == null || !audioManager.isBluetoothScoAvailableOffCall()) {
             Log.e(Config.LOGTAG, "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 (!getBluetoothProfileProxy(
@@ -149,16 +134,20 @@ public class AppRTCBluetoothManager {
         // Register receiver for change in audio connection state of the Headset profile.
         bluetoothHeadsetFilter.addAction(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED);
         registerReceiver(bluetoothHeadsetReceiver, bluetoothHeadsetFilter);
-        Log.d(Config.LOGTAG, "HEADSET profile state: "
-                + stateToString(bluetoothAdapter.getProfileConnectionState(BluetoothProfile.HEADSET)));
+        if (hasBluetoothConnectPermission()) {
+            Log.d(
+                    Config.LOGTAG,
+                    "HEADSET profile state: "
+                            + stateToString(
+                                    bluetoothAdapter.getProfileConnectionState(
+                                            BluetoothProfile.HEADSET)));
+        }
         Log.d(Config.LOGTAG, "Bluetooth proxy for headset profile has started");
         bluetoothState = State.HEADSET_UNAVAILABLE;
         Log.d(Config.LOGTAG, "start done: BT state=" + bluetoothState);
     }
 
-    /**
-     * Stops and closes all components related to Bluetooth audio.
-     */
+    /** Stops and closes all components related to Bluetooth audio. */
     public void stop() {
         ThreadUtils.checkIsOnMainThread();
         Log.d(Config.LOGTAG, "stop: BT state=" + bluetoothState);
@@ -184,23 +173,29 @@ public class AppRTCBluetoothManager {
     }
 
     /**
-     * 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.
+     * 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".
      */
     public boolean startScoAudio() {
         ThreadUtils.checkIsOnMainThread();
-        Log.d(Config.LOGTAG, "startSco: BT state=" + bluetoothState + ", "
-                + "attempts: " + scoConnectionAttempts + ", "
-                + "SCO is on: " + isScoOn());
+        Log.d(
+                Config.LOGTAG,
+                "startSco: BT state="
+                        + bluetoothState
+                        + ", "
+                        + "attempts: "
+                        + scoConnectionAttempts
+                        + ", "
+                        + "SCO is on: "
+                        + isScoOn());
         if (scoConnectionAttempts >= MAX_SCO_CONNECTION_ATTEMPTS) {
             Log.e(Config.LOGTAG, "BT SCO connection fails - no more attempts");
             return false;
@@ -213,24 +208,29 @@ public class AppRTCBluetoothManager {
         Log.d(Config.LOGTAG, "Starting Bluetooth SCO and waits for ACTION_AUDIO_STATE_CHANGED...");
         // 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.
+        // intent ACTION_SCO_AUDIO_STATE_UPDATED and wait for the state to be
+        // SCO_AUDIO_STATE_CONNECTED.
         bluetoothState = State.SCO_CONNECTING;
         audioManager.startBluetoothSco();
         audioManager.setBluetoothScoOn(true);
         scoConnectionAttempts++;
         startTimer();
-        Log.d(Config.LOGTAG, "startScoAudio done: BT state=" + bluetoothState + ", "
-                + "SCO is on: " + isScoOn());
+        Log.d(
+                Config.LOGTAG,
+                "startScoAudio done: BT state="
+                        + bluetoothState
+                        + ", "
+                        + "SCO is on: "
+                        + isScoOn());
         return true;
     }
 
-    /**
-     * Stops Bluetooth SCO connection with remote device.
-     */
+    /** Stops Bluetooth SCO connection with remote device. */
     public void stopScoAudio() {
         ThreadUtils.checkIsOnMainThread();
-        Log.d(Config.LOGTAG, "stopScoAudio: BT state=" + bluetoothState + ", "
-                + "SCO is on: " + isScoOn());
+        Log.d(
+                Config.LOGTAG,
+                "stopScoAudio: BT state=" + bluetoothState + ", " + "SCO is on: " + isScoOn());
         if (bluetoothState != State.SCO_CONNECTING && bluetoothState != State.SCO_CONNECTED) {
             return;
         }
@@ -238,17 +238,18 @@ public class AppRTCBluetoothManager {
         audioManager.stopBluetoothSco();
         audioManager.setBluetoothScoOn(false);
         bluetoothState = State.SCO_DISCONNECTING;
-        Log.d(Config.LOGTAG, "stopScoAudio done: BT state=" + bluetoothState + ", "
-                + "SCO is on: " + isScoOn());
+        Log.d(
+                Config.LOGTAG,
+                "stopScoAudio done: BT state=" + bluetoothState + ", " + "SCO is on: " + isScoOn());
     }
 
     /**
-     * Use the BluetoothHeadset proxy object (controls the Bluetooth Headset
-     * Service via IPC) to update the list of connected devices for the HEADSET
-     * profile. The internal state will change to HEADSET_UNAVAILABLE or to
-     * HEADSET_AVAILABLE and |bluetoothDevice| will be mapped to the connected
-     * device if available.
+     * Use the BluetoothHeadset proxy object (controls the Bluetooth Headset Service via IPC) to
+     * update the list of connected devices for the HEADSET profile. The internal state will change
+     * to HEADSET_UNAVAILABLE or to HEADSET_AVAILABLE and |bluetoothDevice| will be mapped to the
+     * connected device if available.
      */
+    @SuppressLint("MissingPermission")
     public void updateDevice() {
         if (bluetoothState == State.UNINITIALIZED || bluetoothHeadset == null) {
             return;
@@ -257,7 +258,12 @@ public class AppRTCBluetoothManager {
         // Get connected devices for the headset profile. Returns the set of
         // devices which are in state STATE_CONNECTED. The BluetoothDevice class
         // is just a thin wrapper for a Bluetooth hardware address.
-        List<BluetoothDevice> devices = bluetoothHeadset.getConnectedDevices();
+        final List<BluetoothDevice> devices;
+        if (hasBluetoothConnectPermission()) {
+            devices = bluetoothHeadset.getConnectedDevices();
+        } else {
+            devices = ImmutableList.of();
+        }
         if (devices.isEmpty()) {
             bluetoothDevice = null;
             bluetoothState = State.HEADSET_UNAVAILABLE;
@@ -266,17 +272,21 @@ public class AppRTCBluetoothManager {
             // Always use first device in list. Android only supports one device.
             bluetoothDevice = devices.get(0);
             bluetoothState = State.HEADSET_AVAILABLE;
-            Log.d(Config.LOGTAG, "Connected bluetooth headset: "
-                    + "name=" + bluetoothDevice.getName() + ", "
-                    + "state=" + stateToString(bluetoothHeadset.getConnectionState(bluetoothDevice))
-                    + ", SCO audio=" + bluetoothHeadset.isAudioConnected(bluetoothDevice));
+            Log.d(
+                    Config.LOGTAG,
+                    "Connected bluetooth headset: "
+                            + "name="
+                            + bluetoothDevice.getName()
+                            + ", "
+                            + "state="
+                            + stateToString(bluetoothHeadset.getConnectionState(bluetoothDevice))
+                            + ", SCO audio="
+                            + bluetoothHeadset.isAudioConnected(bluetoothDevice));
         }
         Log.d(Config.LOGTAG, "updateDevice done: BT state=" + bluetoothState);
     }
 
-    /**
-     * Stubs for test mocks.
-     */
+    /** Stubs for test mocks. */
     @Nullable
     protected AudioManager getAudioManager(Context context) {
         return (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
@@ -295,52 +305,31 @@ public class AppRTCBluetoothManager {
         return bluetoothAdapter.getProfileProxy(context, listener, profile);
     }
 
-    protected boolean hasPermission(Context context, String permission) {
-        return apprtcContext.checkPermission(permission, Process.myPid(), Process.myUid())
-                == PackageManager.PERMISSION_GRANTED;
-    }
-
-    /**
-     * Logs the state of the local Bluetooth adapter.
-     */
-    @SuppressLint("HardwareIds")
-    protected void logBluetoothAdapterInfo(BluetoothAdapter localAdapter) {
-        Log.d(Config.LOGTAG, "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.isEmpty()) {
-            Log.d(Config.LOGTAG, "paired devices:");
-            for (BluetoothDevice device : pairedDevices) {
-                Log.d(Config.LOGTAG, " name=" + device.getName() + ", address=" + device.getAddress());
-            }
+    protected boolean hasBluetoothConnectPermission() {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+            return ActivityCompat.checkSelfPermission(
+                            apprtcContext, Manifest.permission.BLUETOOTH_CONNECT)
+                    == PackageManager.PERMISSION_GRANTED;
+        } else {
+            return true;
         }
     }
 
-    /**
-     * Ensures that the audio manager updates its list of available audio devices.
-     */
+    /** Ensures that the audio manager updates its list of available audio devices. */
     private void updateAudioDeviceState() {
         ThreadUtils.checkIsOnMainThread();
         Log.d(Config.LOGTAG, "updateAudioDeviceState");
         apprtcAudioManager.updateAudioDeviceState();
     }
 
-    /**
-     * Starts timer which times out after BLUETOOTH_SCO_TIMEOUT_MS milliseconds.
-     */
+    /** Starts timer which times out after BLUETOOTH_SCO_TIMEOUT_MS milliseconds. */
     private void startTimer() {
         ThreadUtils.checkIsOnMainThread();
         Log.d(Config.LOGTAG, "startTimer");
         handler.postDelayed(bluetoothTimeoutRunnable, BLUETOOTH_SCO_TIMEOUT_MS);
     }
 
-    /**
-     * Cancels any outstanding timer tasks.
-     */
+    /** Cancels any outstanding timer tasks. */
     private void cancelTimer() {
         ThreadUtils.checkIsOnMainThread();
         Log.d(Config.LOGTAG, "cancelTimer");
@@ -348,23 +337,36 @@ public class AppRTCBluetoothManager {
     }
 
     /**
-     * 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.
+     * 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.
      */
+    @SuppressLint("MissingPermission")
     private void bluetoothTimeout() {
         ThreadUtils.checkIsOnMainThread();
         if (bluetoothState == State.UNINITIALIZED || bluetoothHeadset == null) {
             return;
         }
-        Log.d(Config.LOGTAG, "bluetoothTimeout: BT state=" + bluetoothState + ", "
-                + "attempts: " + scoConnectionAttempts + ", "
-                + "SCO is on: " + isScoOn());
+        Log.d(
+                Config.LOGTAG,
+                "bluetoothTimeout: BT state="
+                        + bluetoothState
+                        + ", "
+                        + "attempts: "
+                        + scoConnectionAttempts
+                        + ", "
+                        + "SCO is on: "
+                        + isScoOn());
         if (bluetoothState != State.SCO_CONNECTING) {
             return;
         }
         // Bluetooth SCO should be connecting; check the latest result.
         boolean scoConnected = false;
-        List<BluetoothDevice> devices = bluetoothHeadset.getConnectedDevices();
+        final List<BluetoothDevice> devices;
+        if (hasBluetoothConnectPermission()) {
+            devices = bluetoothHeadset.getConnectedDevices();
+        } else {
+            devices = Collections.emptyList();
+        }
         if (devices.size() > 0) {
             bluetoothDevice = devices.get(0);
             if (bluetoothHeadset.isAudioConnected(bluetoothDevice)) {
@@ -387,16 +389,12 @@ public class AppRTCBluetoothManager {
         Log.d(Config.LOGTAG, "bluetoothTimeout done: BT state=" + bluetoothState);
     }
 
-    /**
-     * Checks whether audio uses Bluetooth SCO.
-     */
+    /** Checks whether audio uses Bluetooth SCO. */
     private boolean isScoOn() {
         return audioManager.isBluetoothScoOn();
     }
 
-    /**
-     * Converts BluetoothAdapter states into local string representations.
-     */
+    /** Converts BluetoothAdapter states into local string representations. */
     private String stateToString(int state) {
         switch (state) {
             case BluetoothAdapter.STATE_DISCONNECTED:
@@ -412,11 +410,13 @@ public class AppRTCBluetoothManager {
             case BluetoothAdapter.STATE_ON:
                 return "ON";
             case BluetoothAdapter.STATE_TURNING_OFF:
-                // Indicates the local Bluetooth adapter is turning off. Local clients should immediately
+                // Indicates the local Bluetooth adapter is turning off. Local clients should
+                // immediately
                 // attempt graceful disconnection of any remote links.
                 return "TURNING_OFF";
             case BluetoothAdapter.STATE_TURNING_ON:
-                // Indicates the local Bluetooth adapter is turning on. However local clients should wait
+                // Indicates the local Bluetooth adapter is turning on. However local clients should
+                // wait
                 // for STATE_ON before attempting to use the adapter.
                 return "TURNING_ON";
             default:
@@ -457,7 +457,9 @@ public class AppRTCBluetoothManager {
             if (profile != BluetoothProfile.HEADSET || bluetoothState == State.UNINITIALIZED) {
                 return;
             }
-            Log.d(Config.LOGTAG, "BluetoothServiceListener.onServiceConnected: BT state=" + bluetoothState);
+            Log.d(
+                    Config.LOGTAG,
+                    "BluetoothServiceListener.onServiceConnected: BT state=" + bluetoothState);
             // Android only supports one connected Bluetooth Headset at a time.
             bluetoothHeadset = (BluetoothHeadset) proxy;
             updateAudioDeviceState();
@@ -470,7 +472,9 @@ public class AppRTCBluetoothManager {
             if (profile != BluetoothProfile.HEADSET || bluetoothState == State.UNINITIALIZED) {
                 return;
             }
-            Log.d(Config.LOGTAG, "BluetoothServiceListener.onServiceDisconnected: BT state=" + bluetoothState);
+            Log.d(
+                    Config.LOGTAG,
+                    "BluetoothServiceListener.onServiceDisconnected: BT state=" + bluetoothState);
             stopScoAudio();
             bluetoothHeadset = null;
             bluetoothDevice = null;
@@ -495,12 +499,20 @@ public class AppRTCBluetoothManager {
             // headset while audio is active using another audio device.
             if (action.equals(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED)) {
                 final int state =
-                        intent.getIntExtra(BluetoothHeadset.EXTRA_STATE, BluetoothHeadset.STATE_DISCONNECTED);
-                Log.d(Config.LOGTAG, "BluetoothHeadsetBroadcastReceiver.onReceive: "
-                        + "a=ACTION_CONNECTION_STATE_CHANGED, "
-                        + "s=" + stateToString(state) + ", "
-                        + "sb=" + isInitialStickyBroadcast() + ", "
-                        + "BT state: " + bluetoothState);
+                        intent.getIntExtra(
+                                BluetoothHeadset.EXTRA_STATE, BluetoothHeadset.STATE_DISCONNECTED);
+                Log.d(
+                        Config.LOGTAG,
+                        "BluetoothHeadsetBroadcastReceiver.onReceive: "
+                                + "a=ACTION_CONNECTION_STATE_CHANGED, "
+                                + "s="
+                                + stateToString(state)
+                                + ", "
+                                + "sb="
+                                + isInitialStickyBroadcast()
+                                + ", "
+                                + "BT state: "
+                                + bluetoothState);
                 if (state == BluetoothHeadset.STATE_CONNECTED) {
                     scoConnectionAttempts = 0;
                     updateAudioDeviceState();
@@ -516,13 +528,22 @@ public class AppRTCBluetoothManager {
                 // Change in the audio (SCO) connection state of the Headset profile.
                 // Typically received after call to startScoAudio() has finalized.
             } else if (action.equals(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED)) {
-                final int state = intent.getIntExtra(
-                        BluetoothHeadset.EXTRA_STATE, BluetoothHeadset.STATE_AUDIO_DISCONNECTED);
-                Log.d(Config.LOGTAG, "BluetoothHeadsetBroadcastReceiver.onReceive: "
-                        + "a=ACTION_AUDIO_STATE_CHANGED, "
-                        + "s=" + stateToString(state) + ", "
-                        + "sb=" + isInitialStickyBroadcast() + ", "
-                        + "BT state: " + bluetoothState);
+                final int state =
+                        intent.getIntExtra(
+                                BluetoothHeadset.EXTRA_STATE,
+                                BluetoothHeadset.STATE_AUDIO_DISCONNECTED);
+                Log.d(
+                        Config.LOGTAG,
+                        "BluetoothHeadsetBroadcastReceiver.onReceive: "
+                                + "a=ACTION_AUDIO_STATE_CHANGED, "
+                                + "s="
+                                + stateToString(state)
+                                + ", "
+                                + "sb="
+                                + isInitialStickyBroadcast()
+                                + ", "
+                                + "BT state: "
+                                + bluetoothState);
                 if (state == BluetoothHeadset.STATE_AUDIO_CONNECTED) {
                     cancelTimer();
                     if (bluetoothState == State.SCO_CONNECTING) {
@@ -531,14 +552,18 @@ public class AppRTCBluetoothManager {
                         scoConnectionAttempts = 0;
                         updateAudioDeviceState();
                     } else {
-                        Log.w(Config.LOGTAG, "Unexpected state BluetoothHeadset.STATE_AUDIO_CONNECTED");
+                        Log.w(
+                                Config.LOGTAG,
+                                "Unexpected state BluetoothHeadset.STATE_AUDIO_CONNECTED");
                     }
                 } else if (state == BluetoothHeadset.STATE_AUDIO_CONNECTING) {
                     Log.d(Config.LOGTAG, "+++ Bluetooth audio SCO is now connecting...");
                 } else if (state == BluetoothHeadset.STATE_AUDIO_DISCONNECTED) {
                     Log.d(Config.LOGTAG, "+++ Bluetooth audio SCO is now disconnected");
                     if (isInitialStickyBroadcast()) {
-                        Log.d(Config.LOGTAG, "Ignore STATE_AUDIO_DISCONNECTED initial sticky broadcast.");
+                        Log.d(
+                                Config.LOGTAG,
+                                "Ignore STATE_AUDIO_DISCONNECTED initial sticky broadcast.");
                         return;
                     }
                     updateAudioDeviceState();
@@ -547,4 +572,4 @@ public class AppRTCBluetoothManager {
             Log.d(Config.LOGTAG, "onReceive done: BT state=" + bluetoothState);
         }
     }
-}
+}