android.bluetooth.BluetoothHeadset

Here are the examples of the java api android.bluetooth.BluetoothHeadset taken from open source projects. By voting up you can indicate which examples are most useful and appropriate.

25 Examples 7

19 Source : VoiceMediator.java
with Apache License 2.0
from LingjuAI

public void setBluetoothHeadset(BluetoothHeadset bluetoothHeadset) {
    this.bluetoothHeadset = bluetoothHeadset;
}

18 Source : BluetoothStateManager.java
with GNU General Public License v3.0
from XecureIT

public clreplaced BluetoothStateManager {

    private static final String TAG = BluetoothStateManager.clreplaced.getSimpleName();

    private enum ScoConnection {

        DISCONNECTED, IN_PROGRESS, CONNECTED
    }

    private final Object LOCK = new Object();

    private final Context context;

    private final BluetoothAdapter bluetoothAdapter;

    private final BluetoothScoReceiver bluetoothScoReceiver;

    private final BluetoothConnectionReceiver bluetoothConnectionReceiver;

    private final BluetoothStateListener listener;

    private BluetoothHeadset bluetoothHeadset = null;

    private ScoConnection scoConnection = ScoConnection.DISCONNECTED;

    private boolean wantsConnection = false;

    public BluetoothStateManager(@NonNull Context context, @Nullable BluetoothStateListener listener) {
        this.context = context.getApplicationContext();
        this.bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
        this.bluetoothScoReceiver = new BluetoothScoReceiver();
        this.bluetoothConnectionReceiver = new BluetoothConnectionReceiver();
        this.listener = listener;
        if (this.bluetoothAdapter == null)
            return;
        requestHeadsetProxyProfile();
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
            context.registerReceiver(bluetoothConnectionReceiver, new IntentFilter(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED));
        }
        Intent sticky = context.registerReceiver(bluetoothScoReceiver, new IntentFilter(getScoChangeIntent()));
        if (sticky != null) {
            bluetoothScoReceiver.onReceive(context, sticky);
        }
        handleBluetoothStateChange();
    }

    public void onDestroy() {
        if (bluetoothHeadset != null && bluetoothAdapter != null && Build.VERSION.SDK_INT >= 11) {
            this.bluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, bluetoothHeadset);
        }
        if (Build.VERSION.SDK_INT >= 11 && bluetoothConnectionReceiver != null) {
            context.unregisterReceiver(bluetoothConnectionReceiver);
        }
        if (bluetoothScoReceiver != null) {
            context.unregisterReceiver(bluetoothScoReceiver);
        }
        this.bluetoothHeadset = null;
    }

    public void setWantsConnection(boolean enabled) {
        synchronized (LOCK) {
            AudioManager audioManager = ServiceUtil.getAudioManager(context);
            this.wantsConnection = enabled;
            if (wantsConnection && isBluetoothAvailable() && scoConnection == ScoConnection.DISCONNECTED) {
                audioManager.startBluetoothSco();
                scoConnection = ScoConnection.IN_PROGRESS;
            } else if (!wantsConnection && scoConnection == ScoConnection.CONNECTED) {
                audioManager.stopBluetoothSco();
                audioManager.setBluetoothSreplaced(false);
                scoConnection = ScoConnection.DISCONNECTED;
            } else if (!wantsConnection && scoConnection == ScoConnection.IN_PROGRESS) {
                audioManager.stopBluetoothSco();
                scoConnection = ScoConnection.DISCONNECTED;
            }
        }
    }

    private void handleBluetoothStateChange() {
        if (listener != null)
            listener.onBluetoothStateChanged(isBluetoothAvailable());
    }

    private boolean isBluetoothAvailable() {
        try {
            synchronized (LOCK) {
                AudioManager audioManager = ServiceUtil.getAudioManager(context);
                if (bluetoothAdapter == null || !bluetoothAdapter.isEnabled())
                    return false;
                if (!audioManager.isBluetoothScoAvailableOffCall())
                    return false;
                if (Build.VERSION.SDK_INT >= 11) {
                    return bluetoothHeadset != null && !bluetoothHeadset.getConnectedDevices().isEmpty();
                } else {
                    return audioManager.isBluetoothSreplaced() || audioManager.isBluetoothA2dpOn();
                }
            }
        } catch (Exception e) {
            Log.w(TAG, e);
            return false;
        }
    }

    private String getScoChangeIntent() {
        if (Build.VERSION.SDK_INT >= 14) {
            return AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED;
        } else {
            return AudioManager.ACTION_SCO_AUDIO_STATE_CHANGED;
        }
    }

    private void requestHeadsetProxyProfile() {
        if (Build.VERSION.SDK_INT >= 11) {
            this.bluetoothAdapter.getProfileProxy(context, new BluetoothProfile.ServiceListener() {

                @RequiresApi(api = Build.VERSION_CODES.HONEYCOMB)
                @Override
                public void onServiceConnected(int profile, BluetoothProfile proxy) {
                    if (profile == BluetoothProfile.HEADSET) {
                        synchronized (LOCK) {
                            bluetoothHeadset = (BluetoothHeadset) proxy;
                        }
                        Intent sticky = context.registerReceiver(null, new IntentFilter(getScoChangeIntent()));
                        bluetoothScoReceiver.onReceive(context, sticky);
                        synchronized (LOCK) {
                            if (wantsConnection && isBluetoothAvailable() && scoConnection == ScoConnection.DISCONNECTED) {
                                AudioManager audioManager = ServiceUtil.getAudioManager(context);
                                audioManager.startBluetoothSco();
                                scoConnection = ScoConnection.IN_PROGRESS;
                            }
                        }
                        handleBluetoothStateChange();
                    }
                }

                @Override
                public void onServiceDisconnected(int profile) {
                    Log.w(TAG, "onServiceDisconnected");
                    if (profile == BluetoothProfile.HEADSET) {
                        bluetoothHeadset = null;
                        handleBluetoothStateChange();
                    }
                }
            }, BluetoothProfile.HEADSET);
        }
    }

    private clreplaced BluetoothScoReceiver extends BroadcastReceiver {

        @Override
        public void onReceive(Context context, Intent intent) {
            if (intent == null)
                return;
            Log.w(TAG, "onReceive");
            synchronized (LOCK) {
                if (getScoChangeIntent().equals(intent.getAction())) {
                    int status = intent.getIntExtra(AudioManager.EXTRA_SCO_AUDIO_STATE, AudioManager.SCO_AUDIO_STATE_ERROR);
                    if (status == AudioManager.SCO_AUDIO_STATE_CONNECTED) {
                        if (Build.VERSION.SDK_INT >= 11 && bluetoothHeadset != null) {
                            List<BluetoothDevice> devices = bluetoothHeadset.getConnectedDevices();
                            for (BluetoothDevice device : devices) {
                                if (bluetoothHeadset.isAudioConnected(device)) {
                                    int deviceClreplaced = device.getBluetoothClreplaced().getDeviceClreplaced();
                                    if (deviceClreplaced == BluetoothClreplaced.Device.AUDIO_VIDEO_HANDSFREE || deviceClreplaced == BluetoothClreplaced.Device.AUDIO_VIDEO_CAR_AUDIO || deviceClreplaced == BluetoothClreplaced.Device.AUDIO_VIDEO_WEARABLE_HEADSET) {
                                        scoConnection = ScoConnection.CONNECTED;
                                        if (wantsConnection) {
                                            AudioManager audioManager = ServiceUtil.getAudioManager(context);
                                            audioManager.setBluetoothSreplaced(true);
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
            handleBluetoothStateChange();
        }
    }

    private clreplaced BluetoothConnectionReceiver extends BroadcastReceiver {

        @Override
        public void onReceive(Context context, Intent intent) {
            Log.w(TAG, "onReceive");
            handleBluetoothStateChange();
        }
    }

    public interface BluetoothStateListener {

        public void onBluetoothStateChanged(boolean isAvailable);
    }
}

18 Source : BluetoothStateManager.java
with GNU General Public License v3.0
from mollyim

public clreplaced BluetoothStateManager {

    private static final String TAG = Log.tag(BluetoothStateManager.clreplaced);

    private enum ScoConnection {

        DISCONNECTED, IN_PROGRESS, CONNECTED
    }

    private final Object LOCK = new Object();

    private final Context context;

    private final BluetoothAdapter bluetoothAdapter;

    private BluetoothScoReceiver bluetoothScoReceiver;

    private BluetoothConnectionReceiver bluetoothConnectionReceiver;

    private final BluetoothStateListener listener;

    private final AtomicBoolean destroyed;

    private volatile ScoConnection scoConnection = ScoConnection.DISCONNECTED;

    private BluetoothHeadset bluetoothHeadset = null;

    private boolean wantsConnection = false;

    public BluetoothStateManager(@NonNull Context context, @Nullable BluetoothStateListener listener) {
        this.context = context.getApplicationContext();
        this.bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
        this.listener = listener;
        this.destroyed = new AtomicBoolean(false);
        if (this.bluetoothAdapter == null)
            return;
        requestHeadsetProxyProfile();
        this.bluetoothConnectionReceiver = new BluetoothConnectionReceiver();
        this.context.registerReceiver(bluetoothConnectionReceiver, new IntentFilter(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED));
        this.bluetoothScoReceiver = new BluetoothScoReceiver();
        Intent sticky = this.context.registerReceiver(bluetoothScoReceiver, new IntentFilter(getScoChangeIntent()));
        if (sticky != null) {
            bluetoothScoReceiver.onReceive(context, sticky);
        }
        handleBluetoothStateChange();
    }

    public void onDestroy() {
        destroyed.set(true);
        if (bluetoothHeadset != null && bluetoothAdapter != null) {
            this.bluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, bluetoothHeadset);
        }
        if (bluetoothConnectionReceiver != null) {
            context.unregisterReceiver(bluetoothConnectionReceiver);
            bluetoothConnectionReceiver = null;
        }
        if (bluetoothScoReceiver != null) {
            context.unregisterReceiver(bluetoothScoReceiver);
            bluetoothScoReceiver = null;
        }
        this.bluetoothHeadset = null;
    }

    public void setWantsConnection(boolean enabled) {
        synchronized (LOCK) {
            AudioManager audioManager = ServiceUtil.getAudioManager(context);
            this.wantsConnection = enabled;
            if (wantsConnection && isBluetoothAvailable() && scoConnection == ScoConnection.DISCONNECTED) {
                audioManager.startBluetoothSco();
                scoConnection = ScoConnection.IN_PROGRESS;
            } else if (!wantsConnection && scoConnection == ScoConnection.CONNECTED) {
                audioManager.stopBluetoothSco();
                audioManager.setBluetoothSreplaced(false);
                scoConnection = ScoConnection.DISCONNECTED;
            } else if (!wantsConnection && scoConnection == ScoConnection.IN_PROGRESS) {
                audioManager.stopBluetoothSco();
                scoConnection = ScoConnection.DISCONNECTED;
            }
        }
    }

    private void handleBluetoothStateChange() {
        if (!destroyed.get()) {
            boolean isBluetoothAvailable = isBluetoothAvailable();
            if (!isBluetoothAvailable) {
                setWantsConnection(false);
            }
            if (listener != null) {
                listener.onBluetoothStateChanged(isBluetoothAvailable);
            }
        }
    }

    private boolean isBluetoothAvailable() {
        try {
            synchronized (LOCK) {
                AudioManager audioManager = ServiceUtil.getAudioManager(context);
                if (bluetoothAdapter == null || !bluetoothAdapter.isEnabled())
                    return false;
                if (!audioManager.isBluetoothScoAvailableOffCall())
                    return false;
                return bluetoothHeadset != null && !bluetoothHeadset.getConnectedDevices().isEmpty();
            }
        } catch (Exception e) {
            Log.w(TAG, e);
            return false;
        }
    }

    private String getScoChangeIntent() {
        return AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED;
    }

    private void requestHeadsetProxyProfile() {
        this.bluetoothAdapter.getProfileProxy(context, new BluetoothProfile.ServiceListener() {

            @Override
            public void onServiceConnected(int profile, BluetoothProfile proxy) {
                if (destroyed.get()) {
                    Log.w(TAG, "Got bluetooth profile event after the service was destroyed. Ignoring.");
                    return;
                }
                if (profile == BluetoothProfile.HEADSET) {
                    synchronized (LOCK) {
                        bluetoothHeadset = (BluetoothHeadset) proxy;
                    }
                    Intent sticky = context.registerReceiver(null, new IntentFilter(getScoChangeIntent()));
                    bluetoothScoReceiver.onReceive(context, sticky);
                    synchronized (LOCK) {
                        if (wantsConnection && isBluetoothAvailable() && scoConnection == ScoConnection.DISCONNECTED) {
                            AudioManager audioManager = ServiceUtil.getAudioManager(context);
                            audioManager.startBluetoothSco();
                            scoConnection = ScoConnection.IN_PROGRESS;
                        }
                    }
                    handleBluetoothStateChange();
                }
            }

            @Override
            public void onServiceDisconnected(int profile) {
                Log.i(TAG, "onServiceDisconnected");
                if (profile == BluetoothProfile.HEADSET) {
                    bluetoothHeadset = null;
                    handleBluetoothStateChange();
                }
            }
        }, BluetoothProfile.HEADSET);
    }

    private clreplaced BluetoothScoReceiver extends BroadcastReceiver {

        @Override
        public void onReceive(Context context, Intent intent) {
            if (intent == null)
                return;
            Log.i(TAG, "onReceive");
            synchronized (LOCK) {
                if (getScoChangeIntent().equals(intent.getAction())) {
                    int status = intent.getIntExtra(AudioManager.EXTRA_SCO_AUDIO_STATE, AudioManager.SCO_AUDIO_STATE_ERROR);
                    if (status == AudioManager.SCO_AUDIO_STATE_CONNECTED) {
                        if (bluetoothHeadset != null) {
                            List<BluetoothDevice> devices = bluetoothHeadset.getConnectedDevices();
                            for (BluetoothDevice device : devices) {
                                if (bluetoothHeadset.isAudioConnected(device)) {
                                    scoConnection = ScoConnection.CONNECTED;
                                    if (wantsConnection) {
                                        AudioManager audioManager = ServiceUtil.getAudioManager(context);
                                        audioManager.setBluetoothSreplaced(true);
                                    }
                                }
                            }
                        }
                    } else if (status == AudioManager.SCO_AUDIO_STATE_DISCONNECTED) {
                        setWantsConnection(false);
                    }
                }
            }
            handleBluetoothStateChange();
        }
    }

    private clreplaced BluetoothConnectionReceiver extends BroadcastReceiver {

        @Override
        public void onReceive(Context context, Intent intent) {
            Log.i(TAG, "onReceive");
            handleBluetoothStateChange();
        }
    }

    public interface BluetoothStateListener {

        public void onBluetoothStateChanged(boolean isAvailable);
    }
}

18 Source : BluetoothProfileServiceTemplate.java
with Apache License 2.0
from devyok

protected boolean connect2(BluetoothDevice device, BluetoothProfile profile) {
    if (this.profileType == BluetoothProfile.A2DP) {
        BluetoothA2dp bluetoothA2dp = (BluetoothA2dp) profile;
        return bluetoothA2dp.connect(device);
    } else if (this.profileType == BluetoothProfile.HEADSET) {
        BluetoothHeadset bluetoothHeadset = (BluetoothHeadset) profile;
        return bluetoothHeadset.connect(device);
    }
    return false;
}

18 Source : BluetoothProfileServiceTemplate.java
with Apache License 2.0
from devyok

protected boolean setPriority2(int priority, BluetoothDevice device, BluetoothProfile profile) {
    if (this.profileType == BluetoothProfile.A2DP) {
        BluetoothA2dp bluetoothA2dp = (BluetoothA2dp) profile;
        return bluetoothA2dp.setPriority(device, priority);
    } else if (this.profileType == BluetoothProfile.HEADSET) {
        BluetoothHeadset bluetoothHeadset = (BluetoothHeadset) profile;
        return bluetoothHeadset.setPriority(device, priority);
    }
    return false;
}

18 Source : BluetoothStateManager.java
with GNU General Public License v3.0
from bcmapp

public clreplaced BluetoothStateManager {

    private static final String TAG = BluetoothStateManager.clreplaced.getSimpleName();

    private enum ScoConnection {

        DISCONNECTED, IN_PROGRESS, CONNECTED
    }

    private final Object LOCK = new Object();

    private Context context;

    private BluetoothAdapter bluetoothAdapter;

    private BluetoothScoReceiver bluetoothScoReceiver;

    private BluetoothConnectionReceiver bluetoothConnectionReceiver;

    private BluetoothStateListener listener;

    private BluetoothHeadset bluetoothHeadset = null;

    private ScoConnection scoConnection = ScoConnection.DISCONNECTED;

    private boolean wantsConnection = false;

    public BluetoothStateManager(@NonNull Context context, @Nullable BluetoothStateListener listener) {
        this.context = context.getApplicationContext();
        this.bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
        if (this.bluetoothAdapter == null) {
            return;
        }
        this.bluetoothScoReceiver = new BluetoothScoReceiver();
        this.bluetoothConnectionReceiver = new BluetoothConnectionReceiver();
        this.listener = listener;
        requestHeadsetProxyProfile();
        this.context.registerReceiver(bluetoothConnectionReceiver, new IntentFilter(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED));
        Intent sticky = this.context.registerReceiver(bluetoothScoReceiver, new IntentFilter(getScoChangeIntent()));
        if (sticky != null) {
            bluetoothScoReceiver.onReceive(this.context, sticky);
        }
        handleBluetoothStateChange();
    }

    public void terminate() {
        this.listener = null;
    }

    public void onDestroy() {
        try {
            this.listener = null;
            if (bluetoothHeadset != null && bluetoothAdapter != null) {
                bluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, bluetoothHeadset);
                bluetoothHeadset = null;
            }
            if (bluetoothConnectionReceiver != null) {
                context.unregisterReceiver(bluetoothConnectionReceiver);
                bluetoothConnectionReceiver = null;
            }
            if (bluetoothScoReceiver != null) {
                context.unregisterReceiver(bluetoothScoReceiver);
                bluetoothScoReceiver = null;
            }
        } catch (Exception ex) {
            ALog.e("BluetoothStateManager", "onDestroy error", ex);
        }
    }

    public void setWantsConnection(boolean enabled) {
        synchronized (LOCK) {
            AudioManager audioManager = AppUtil.INSTANCE.getAudioManager(context);
            this.wantsConnection = enabled;
            if (wantsConnection && isBluetoothAvailable() && scoConnection == ScoConnection.DISCONNECTED) {
                audioManager.startBluetoothSco();
                scoConnection = ScoConnection.IN_PROGRESS;
            } else if (!wantsConnection && scoConnection == ScoConnection.CONNECTED) {
                audioManager.stopBluetoothSco();
                audioManager.setBluetoothSreplaced(false);
                scoConnection = ScoConnection.DISCONNECTED;
            } else if (!wantsConnection && scoConnection == ScoConnection.IN_PROGRESS) {
                audioManager.stopBluetoothSco();
                scoConnection = ScoConnection.DISCONNECTED;
            }
        }
    }

    private void handleBluetoothStateChange() {
        if (listener != null) {
            listener.onBluetoothStateChanged(isBluetoothAvailable());
        }
    }

    private boolean isBluetoothAvailable() {
        try {
            synchronized (LOCK) {
                AudioManager audioManager = AppUtil.INSTANCE.getAudioManager(context);
                if (bluetoothAdapter == null || !bluetoothAdapter.isEnabled()) {
                    return false;
                }
                if (!audioManager.isBluetoothScoAvailableOffCall()) {
                    return false;
                }
                return bluetoothHeadset != null && !bluetoothHeadset.getConnectedDevices().isEmpty();
            }
        } catch (Exception e) {
            Log.w(TAG, e);
            return false;
        }
    }

    private String getScoChangeIntent() {
        return AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED;
    }

    private void requestHeadsetProxyProfile() {
        this.bluetoothAdapter.getProfileProxy(context, new BluetoothProfile.ServiceListener() {

            @Override
            public void onServiceConnected(int profile, BluetoothProfile proxy) {
                if (profile == BluetoothProfile.HEADSET) {
                    synchronized (LOCK) {
                        bluetoothHeadset = (BluetoothHeadset) proxy;
                    }
                    Intent sticky = context.registerReceiver(null, new IntentFilter(getScoChangeIntent()));
                    if (bluetoothScoReceiver != null) {
                        bluetoothScoReceiver.onReceive(context, sticky);
                    }
                    synchronized (LOCK) {
                        if (wantsConnection && isBluetoothAvailable() && scoConnection == ScoConnection.DISCONNECTED) {
                            AudioManager audioManager = AppUtil.INSTANCE.getAudioManager(context);
                            audioManager.startBluetoothSco();
                            scoConnection = ScoConnection.IN_PROGRESS;
                        }
                    }
                    handleBluetoothStateChange();
                }
            }

            @Override
            public void onServiceDisconnected(int profile) {
                Log.w(TAG, "onServiceDisconnected");
                if (profile == BluetoothProfile.HEADSET) {
                    bluetoothHeadset = null;
                    handleBluetoothStateChange();
                }
            }
        }, BluetoothProfile.HEADSET);
    }

    private clreplaced BluetoothScoReceiver extends BroadcastReceiver {

        @Override
        public void onReceive(Context context, Intent intent) {
            if (intent == null) {
                return;
            }
            Log.w(TAG, "onReceive");
            synchronized (LOCK) {
                if (getScoChangeIntent().equals(intent.getAction())) {
                    int status = intent.getIntExtra(AudioManager.EXTRA_SCO_AUDIO_STATE, AudioManager.SCO_AUDIO_STATE_ERROR);
                    if (status == AudioManager.SCO_AUDIO_STATE_CONNECTED && bluetoothHeadset != null) {
                        List<BluetoothDevice> devices = bluetoothHeadset.getConnectedDevices();
                        for (BluetoothDevice device : devices) {
                            if (bluetoothHeadset.isAudioConnected(device)) {
                                int deviceClreplaced = device.getBluetoothClreplaced().getDeviceClreplaced();
                                if (deviceClreplaced == BluetoothClreplaced.Device.AUDIO_VIDEO_HANDSFREE || deviceClreplaced == BluetoothClreplaced.Device.AUDIO_VIDEO_CAR_AUDIO || deviceClreplaced == BluetoothClreplaced.Device.AUDIO_VIDEO_WEARABLE_HEADSET) {
                                    scoConnection = ScoConnection.CONNECTED;
                                    if (wantsConnection) {
                                        AudioManager audioManager = AppUtil.INSTANCE.getAudioManager(context);
                                        audioManager.setBluetoothSreplaced(true);
                                    }
                                }
                            }
                        }
                    }
                }
            }
            handleBluetoothStateChange();
        }
    }

    private clreplaced BluetoothConnectionReceiver extends BroadcastReceiver {

        @Override
        public void onReceive(Context context, Intent intent) {
            Log.w(TAG, "onReceive");
            handleBluetoothStateChange();
        }
    }

    public interface BluetoothStateListener {

        public void onBluetoothStateChanged(boolean isAvailable);
    }
}

18 Source : BtNativeInterface.java
with Apache License 2.0
from ApolloAuto

public clreplaced BtNativeInterface implements BtInterfaceBase {

    private static final String TAG = BtNativeInterface.clreplaced.getSimpleName();

    private BluetoothAdapter mBluetoothAdapter = null;

    private BluetoothHeadset mBluetoothHeadset = null;

    private Context mContext = null;

    private BtInterfaceBase.BtCallback mBtCallback = null;

    public BtNativeInterface(Context cx) {
        mContext = cx;
        mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
    }

    private void registerReceiver() {
        IntentFilter filter = new IntentFilter();
        filter.addAction(BtUtils.ACTION_PAIRING_REQUEST);
        filter.addAction(BtUtils.ACTION_BOND_STATE_CHANGED);
        filter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
        if (mContext != null) {
            mContext.registerReceiver(mBluetoothReceiver, filter);
        }
    }

    private void unregisterReceiver() {
        if (mContext != null && mBluetoothAdapter != null) {
            mContext.unregisterReceiver(mBluetoothReceiver);
        }
    }

    private void getHeadsetProxy() {
        if (mBluetoothAdapter != null) {
            try {
                boolean ret = mBluetoothAdapter.getProfileProxy(mContext, new BluetoothProfile.ServiceListener() {

                    @Override
                    public void onServiceDisconnected(int profile) {
                        mBluetoothHeadset = null;
                        if (mBtCallback != null) {
                            mBtCallback.onServiceStart(false);
                        }
                        LogUtil.d(TAG, "Disconnect headset proxy!!!");
                    }

                    @Override
                    public void onServiceConnected(int profile, BluetoothProfile proxy) {
                        if (profile == BluetoothProfile.HEADSET && proxy != null) {
                            mBluetoothHeadset = (BluetoothHeadset) proxy;
                            if (mBtCallback != null) {
                                mBtCallback.onServiceStart(true);
                            }
                            LogUtil.d(TAG, "Get headset proxy: " + mBluetoothHeadset);
                        }
                    }
                }, BluetoothProfile.HEADSET);
                LogUtil.d(TAG, "getProfileProxy ret = " + ret);
            } catch (Exception e) {
                e.printStackTrace();
                if (mBtCallback != null) {
                    mBtCallback.onServiceStart(false);
                }
                LogUtil.d(TAG, "getProfileProxy Exception");
            }
        }
    }

    private boolean isPaired(String address) {
        String pairedListString = getBondedDevicesAddress();
        if (pairedListString != "" && pairedListString.contains(address)) {
            return true;
        }
        return false;
    }

    @Override
    public void init(BtInterfaceBase.BtCallback callback) {
        mBtCallback = callback;
        registerReceiver();
        getHeadsetProxy();
    }

    @Override
    public void uninit(BtInterfaceBase.BtCallback callback) {
        unregisterReceiver();
        if (mBluetoothHeadset != null && mBluetoothAdapter != null) {
            mBluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, mBluetoothHeadset);
            LogUtil.d(TAG, "Close headset proxy");
        }
        mBtCallback = null;
    }

    @Override
    public String getAddress() {
        return null;
    }

    @Override
    public String getName() {
        return null;
    }

    @Override
    public String getPincode() {
        return null;
    }

    @Override
    public boolean isEnable() {
        if (mBluetoothAdapter != null) {
            return mBluetoothAdapter.isEnabled();
        }
        return false;
    }

    @Override
    public boolean enable() {
        if (mBluetoothAdapter != null) {
            return mBluetoothAdapter.enable();
        }
        return false;
    }

    @Override
    public boolean disable() {
        if (mBluetoothAdapter != null) {
            return mBluetoothAdapter.disable();
        }
        return false;
    }

    @Override
    public int getHfpConnectionState() {
        int connState = BluetoothProfile.STATE_DISCONNECTED;
        if (mBluetoothAdapter != null) {
            connState = mBluetoothAdapter.getProfileConnectionState(BluetoothProfile.HEADSET);
            return connState;
        }
        return connState;
    }

    @Override
    public String getConnectedDeviceAddress() {
        String addressListString = "";
        if (mBluetoothHeadset != null) {
            List<BluetoothDevice> connectedDevices = mBluetoothHeadset.getConnectedDevices();
            for (BluetoothDevice device : connectedDevices) {
                if (device == null) {
                    continue;
                }
                String address = device.getAddress();
                if (!TextUtils.isEmpty(address)) {
                    addressListString += address + '#';
                }
            }
        }
        return addressListString;
    }

    @Override
    public boolean disconnect() {
        return false;
    }

    @Override
    public String getBondedDevicesAddress() {
        String pairedListString = "";
        if (mBluetoothAdapter == null) {
            return pairedListString;
        }
        Set<BluetoothDevice> pairedSet = mBluetoothAdapter.getBondedDevices();
        if ((pairedSet != null) && pairedSet.size() > 0) {
            for (BluetoothDevice device : pairedSet) {
                if (device == null) {
                    continue;
                }
                String address = device.getAddress();
                if (!TextUtils.isEmpty(address)) {
                    pairedListString += address + '#';
                }
            }
        }
        return pairedListString;
    }

    @Override
    public boolean setPin(String pin) {
        return false;
    }

    @Override
    public boolean setPairingConfirmation(boolean accept, BluetoothDevice device) {
        try {
            boolean isConfirmed = BtUtils.setPairingConfirmation(mBluetoothHeadset.getClreplaced(), device, true);
            LogUtil.i(TAG, "isConfirmed=" + isConfirmed);
            return isConfirmed;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }

    private final BroadcastReceiver mBluetoothReceiver = new BroadcastReceiver() {

        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            LogUtil.d(TAG, "Bluetooth Broadcase receiver: action = " + action);
            if (BtUtils.ACTION_PAIRING_REQUEST.equals(action)) {
                final BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
                int type = intent.getIntExtra(BtUtils.EXTRA_PAIRING_VARIANT, BluetoothDevice.ERROR);
                final String address = device.getAddress();
                if (mBtCallback != null) {
                    mBtCallback.onReceivedPairingRequest(type, device);
                }
            } else if (BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(action)) {
                int state = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_NONE);
                BluetoothDevice bondDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
                String addr = bondDevice.getAddress();
                if (mBtCallback != null) {
                    mBtCallback.onReceivedBondStateChange(state, bondDevice);
                }
            } else if (BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED.equals(action)) {
                BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
                int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, BluetoothProfile.STATE_DISCONNECTED);
                String addr = device.getAddress();
                LogUtil.d(TAG, "BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED: remote addr = " + addr + "state = " + state);
                if (mBtCallback != null) {
                    mBtCallback.onReceivedHfpConnectionStateChanged(state, device);
                }
            }
        }
    };
}

17 Source : BluetoothManager.java
with GNU General Public License v3.0
from treasure-lau

/**
 * @author Sylvain Berfini
 */
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public clreplaced BluetoothManager extends BroadcastReceiver {

    public int PLANTRONICS_BUTTON_PRESS = 1;

    public int PLANTRONICS_BUTTON_LONG_PRESS = 2;

    public int PLANTRONICS_BUTTON_DOUBLE_PRESS = 5;

    public int PLANTRONICS_BUTTON_CALL = 2;

    public int PLANTRONICS_BUTTON_MUTE = 3;

    private static BluetoothManager instance;

    private Context mContext;

    private AudioManager mAudioManager;

    private BluetoothAdapter mBluetoothAdapter;

    private BluetoothHeadset mBluetoothHeadset;

    private BluetoothDevice mBluetoothDevice;

    private BluetoothProfile.ServiceListener mProfileListener;

    private boolean isBluetoothConnected;

    private boolean isScoConnected;

    public static BluetoothManager getInstance() {
        if (instance == null) {
            instance = new BluetoothManager();
        }
        return instance;
    }

    public BluetoothManager() {
        isBluetoothConnected = false;
        if (!ensureInit()) {
            Log.w("[Bluetooth] Manager tried to init but LinphoneService not ready yet...");
        }
        instance = this;
    }

    public void initBluetooth() {
        if (!ensureInit()) {
            Log.w("[Bluetooth] Manager tried to init bluetooth but LinphoneService not ready yet...");
            return;
        }
        IntentFilter filter = new IntentFilter();
        filter.addCategory(BluetoothHeadset.VENDOR_SPECIFIC_HEADSET_EVENT_COMPANY_ID_CATEGORY + "." + BluetoothreplacedignedNumbers.PLANTRONICS);
        filter.addAction(AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED);
        filter.addAction(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED);
        filter.addAction(BluetoothHeadset.ACTION_VENDOR_SPECIFIC_HEADSET_EVENT);
        mContext.registerReceiver(this, filter);
        Log.d("[Bluetooth] Receiver started");
        startBluetooth();
    }

    private void startBluetooth() {
        if (isBluetoothConnected) {
            Log.e("[Bluetooth] Already started, skipping...");
            return;
        }
        mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
        if (mBluetoothAdapter != null && mBluetoothAdapter.isEnabled()) {
            if (mProfileListener != null) {
                Log.w("[Bluetooth] Headset profile was already opened, let's close it");
                mBluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, mBluetoothHeadset);
            }
            mProfileListener = new BluetoothProfile.ServiceListener() {

                public void onServiceConnected(int profile, BluetoothProfile proxy) {
                    if (profile == BluetoothProfile.HEADSET) {
                        Log.d("[Bluetooth] Headset connected");
                        mBluetoothHeadset = (BluetoothHeadset) proxy;
                        isBluetoothConnected = true;
                    }
                }

                public void onServiceDisconnected(int profile) {
                    if (profile == BluetoothProfile.HEADSET) {
                        mBluetoothHeadset = null;
                        isBluetoothConnected = false;
                        Log.d("[Bluetooth] Headset disconnected");
                        LinphoneManager.getInstance().routeAudioToReceiver();
                    }
                }
            };
            boolean success = mBluetoothAdapter.getProfileProxy(mContext, mProfileListener, BluetoothProfile.HEADSET);
            if (!success) {
                Log.e("[Bluetooth] getProfileProxy failed !");
            }
        } else {
            Log.w("[Bluetooth] Interface disabled on device");
        }
    }

    private boolean ensureInit() {
        if (mBluetoothAdapter == null) {
            mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
        }
        if (mContext == null) {
            if (LinphoneService.isReady()) {
                mContext = LinphoneService.instance().getApplicationContext();
            } else {
                return false;
            }
        }
        if (mContext != null && mAudioManager == null) {
            mAudioManager = ((AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE));
        }
        return true;
    }

    public boolean routeAudioToBluetooth() {
        ensureInit();
        if (mBluetoothAdapter != null && mBluetoothAdapter.isEnabled() && mAudioManager != null && mAudioManager.isBluetoothScoAvailableOffCall()) {
            if (isBluetoothHeadsetAvailable()) {
                if (mAudioManager != null && !mAudioManager.isBluetoothSreplaced()) {
                    Log.d("[Bluetooth] SCO off, let's start it");
                    mAudioManager.setBluetoothSreplaced(true);
                    mAudioManager.startBluetoothSco();
                }
            } else {
                return false;
            }
            // Hack to ensure bluetooth sco is really running
            boolean ok = isUsingBluetoothAudioRoute();
            int retries = 0;
            while (!ok && retries < 5) {
                retries++;
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                }
                if (mAudioManager != null) {
                    mAudioManager.setBluetoothSreplaced(true);
                    mAudioManager.startBluetoothSco();
                }
                ok = isUsingBluetoothAudioRoute();
            }
            if (ok) {
                if (retries > 0) {
                    Log.d("[Bluetooth] Audio route ok after " + retries + " retries");
                } else {
                    Log.d("[Bluetooth] Audio route ok");
                }
            } else {
                Log.d("[Bluetooth] Audio route still not ok...");
            }
            return ok;
        }
        return false;
    }

    public boolean isUsingBluetoothAudioRoute() {
        return mBluetoothHeadset != null && mBluetoothHeadset.isAudioConnected(mBluetoothDevice) && isScoConnected;
    }

    public boolean isBluetoothHeadsetAvailable() {
        ensureInit();
        if (mBluetoothAdapter != null && mBluetoothAdapter.isEnabled() && mAudioManager != null && mAudioManager.isBluetoothScoAvailableOffCall()) {
            boolean isHeadsetConnected = false;
            if (mBluetoothHeadset != null) {
                List<BluetoothDevice> devices = mBluetoothHeadset.getConnectedDevices();
                mBluetoothDevice = null;
                for (final BluetoothDevice dev : devices) {
                    if (mBluetoothHeadset.getConnectionState(dev) == BluetoothHeadset.STATE_CONNECTED) {
                        mBluetoothDevice = dev;
                        isHeadsetConnected = true;
                        break;
                    }
                }
                Log.d(isHeadsetConnected ? "[Bluetooth] Headset found, bluetooth audio route available" : "[Bluetooth] No headset found, bluetooth audio route unavailable");
            }
            return isHeadsetConnected;
        }
        return false;
    }

    public void disableBluetoothSCO() {
        if (mAudioManager != null && mAudioManager.isBluetoothSreplaced()) {
            mAudioManager.stopBluetoothSco();
            mAudioManager.setBluetoothSreplaced(false);
            // Hack to ensure bluetooth sco is really stopped
            int retries = 0;
            while (isScoConnected && retries < 10) {
                retries++;
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                }
                mAudioManager.stopBluetoothSco();
                mAudioManager.setBluetoothSreplaced(false);
            }
            Log.w("[Bluetooth] SCO disconnected!");
        }
    }

    public void stopBluetooth() {
        Log.w("[Bluetooth] Stopping...");
        isBluetoothConnected = false;
        disableBluetoothSCO();
        if (mBluetoothAdapter != null && mProfileListener != null && mBluetoothHeadset != null) {
            mBluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, mBluetoothHeadset);
            mProfileListener = null;
        }
        mBluetoothDevice = null;
        Log.w("[Bluetooth] Stopped!");
        if (LinphoneManager.isInstanciated()) {
            LinphoneManager.getInstance().routeAudioToReceiver();
        }
    }

    public void destroy() {
        try {
            stopBluetooth();
            try {
                mContext.unregisterReceiver(this);
                Log.d("[Bluetooth] Receiver stopped");
            } catch (Exception e) {
            }
        } catch (Exception e) {
            Log.e(e);
        }
    }

    public void onReceive(Context context, Intent intent) {
        if (!LinphoneManager.isInstanciated())
            return;
        String action = intent.getAction();
        if (AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED.equals(action)) {
            int state = intent.getIntExtra(AudioManager.EXTRA_SCO_AUDIO_STATE, 0);
            if (state == AudioManager.SCO_AUDIO_STATE_CONNECTED) {
                Log.d("[Bluetooth] SCO state: connected");
                // LinphoneManager.getInstance().audioStateChanged(AudioState.BLUETOOTH);
                isScoConnected = true;
            } else if (state == AudioManager.SCO_AUDIO_STATE_DISCONNECTED) {
                Log.d("[Bluetooth] SCO state: disconnected");
                // LinphoneManager.getInstance().audioStateChanged(AudioState.SPEAKER);
                isScoConnected = false;
            } else {
                Log.d("[Bluetooth] SCO state: " + state);
            }
        } else if (BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED.equals(action)) {
            int state = intent.getIntExtra(BluetoothAdapter.EXTRA_CONNECTION_STATE, BluetoothAdapter.STATE_DISCONNECTED);
            if (state == 0) {
                Log.d("[Bluetooth] State: disconnected");
                stopBluetooth();
            } else if (state == 2) {
                Log.d("[Bluetooth] State: connected");
                startBluetooth();
            } else {
                Log.d("[Bluetooth] State: " + state);
            }
        } else if (intent.getAction().equals(BluetoothHeadset.ACTION_VENDOR_SPECIFIC_HEADSET_EVENT)) {
            String command = intent.getExtras().getString(BluetoothHeadset.EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD);
            // int type = intent.getExtras().getInt(BluetoothHeadset.EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD_TYPE);
            Object[] args = (Object[]) intent.getExtras().get(BluetoothHeadset.EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_ARGS);
            String eventName = (String) args[0];
            if (eventName.equals("BUTTON") && args.length >= 3) {
                Integer buttonID = (Integer) args[1];
                Integer mode = (Integer) args[2];
                Log.d("[Bluetooth] Event: " + command + " : " + eventName + ", id = " + buttonID + " (" + mode + ")");
            }
        }
    }
}

17 Source : AppRTCBluetoothManager.java
with Apache License 2.0
from sokunmin

/**
 * AppRTCProximitySensor manages functions related to Bluetoth devices in the
 * AppRTC demo.
 */
public clreplaced AppRTCBluetoothManager {

    private static final String TAG = "AppRTCBluetoothManager";

    // 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;

    // Bluetooth connection state.
    public enum State {

        // Bluetooth is not available; no adapter or Bluetooth is off.
        UNINITIALIZED,
        // Bluetooth error happened when trying to start Bluetooth.
        ERROR,
        // Bluetooth proxy object for the Headset profile exists, but no connected headset devices,
        // SCO is not started or disconnected.
        HEADSET_UNAVAILABLE,
        // Bluetooth proxy object for the Headset profile 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
    }

    private final Context apprtcContext;

    private final AppRTCAudioManager apprtcAudioManager;

    private final AudioManager audioManager;

    private final Handler handler;

    int scoConnectionAttempts;

    private State bluetoothState;

    private final BluetoothProfile.ServiceListener bluetoothServiceListener;

    private BluetoothAdapter bluetoothAdapter;

    private BluetoothHeadset bluetoothHeadset;

    private BluetoothDevice bluetoothDevice;

    private final BroadcastReceiver bluetoothHeadsetReceiver;

    // 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();
        }
    };

    /**
     * Implementation of an interface that notifies BluetoothProfile IPC clients when they have been
     * connected to or disconnected from the service.
     */
    private clreplaced BluetoothServiceListener implements BluetoothProfile.ServiceListener {

        @Override
        public // connection and perform other operations that are relevant to the headset profile.
        void onServiceConnected(int profile, BluetoothProfile proxy) {
            if (profile != BluetoothProfile.HEADSET || bluetoothState == State.UNINITIALIZED) {
                return;
            }
            Log.d(TAG, "BluetoothServiceListener.onServiceConnected: BT state=" + bluetoothState);
            // Android only supports one connected Bluetooth Headset at a time.
            bluetoothHeadset = (BluetoothHeadset) proxy;
            updateAudioDeviceState();
            Log.d(TAG, "onServiceConnected done: BT state=" + bluetoothState);
        }

        @Override
        public /**
         * Notifies the client when the proxy object has been disconnected from the service.
         */
        void onServiceDisconnected(int profile) {
            if (profile != BluetoothProfile.HEADSET || bluetoothState == State.UNINITIALIZED) {
                return;
            }
            Log.d(TAG, "BluetoothServiceListener.onServiceDisconnected: BT state=" + bluetoothState);
            stopScoAudio();
            bluetoothHeadset = null;
            bluetoothDevice = null;
            bluetoothState = State.HEADSET_UNAVAILABLE;
            updateAudioDeviceState();
            Log.d(TAG, "onServiceDisconnected done: BT state=" + bluetoothState);
        }
    }

    // Intent broadcast receiver which handles changes in Bluetooth device availability.
    // Detects headset changes and Bluetooth SCO state changes.
    private clreplaced BluetoothHeadsetBroadcastReceiver extends BroadcastReceiver {

        @Override
        public void onReceive(Context context, Intent intent) {
            if (bluetoothState == State.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. Typically received when user turns on a BT
            // 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(TAG, "BluetoothHeadsetBroadcastReceiver.onReceive: " + "a=ACTION_CONNECTION_STATE_CHANGED, " + "s=" + stateToString(state) + ", " + "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.
                    stopScoAudio();
                    updateAudioDeviceState();
                }
            // 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(TAG, "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) {
                        Log.d(TAG, "+++ Bluetooth audio SCO is now connected");
                        bluetoothState = State.SCO_CONNECTED;
                        scoConnectionAttempts = 0;
                        updateAudioDeviceState();
                    } else {
                        Log.w(TAG, "Unexpected state BluetoothHeadset.STATE_AUDIO_CONNECTED");
                    }
                } else if (state == BluetoothHeadset.STATE_AUDIO_CONNECTING) {
                    Log.d(TAG, "+++ Bluetooth audio SCO is now connecting...");
                } else if (state == BluetoothHeadset.STATE_AUDIO_DISCONNECTED) {
                    Log.d(TAG, "+++ Bluetooth audio SCO is now disconnected");
                    if (isInitialStickyBroadcast()) {
                        Log.d(TAG, "Ignore STATE_AUDIO_DISCONNECTED initial sticky broadcast.");
                        return;
                    }
                    updateAudioDeviceState();
                }
            }
            Log.d(TAG, "onReceive done: BT state=" + bluetoothState);
        }
    }

    /**
     * Construction.
     */
    static AppRTCBluetoothManager create(Context context, AppRTCAudioManager audioManager) {
        Log.d(TAG, "create" + AppRTCUtils.getThreadInfo());
        return new AppRTCBluetoothManager(context, audioManager);
    }

    protected AppRTCBluetoothManager(Context context, AppRTCAudioManager audioManager) {
        Log.d(TAG, "ctor");
        ThreadUtils.checkIsOnMainThread();
        apprtcContext = context;
        apprtcAudioManager = audioManager;
        this.audioManager = getAudioManager(context);
        bluetoothState = State.UNINITIALIZED;
        bluetoothServiceListener = new BluetoothServiceListener();
        bluetoothHeadsetReceiver = new BluetoothHeadsetBroadcastReceiver();
        handler = new Handler(Looper.getMainLooper());
    }

    /**
     * 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.
     */
    public void start() {
        ThreadUtils.checkIsOnMainThread();
        Log.d(TAG, "start");
        if (!hasPermission(apprtcContext, android.Manifest.permission.BLUETOOTH)) {
            Log.w(TAG, "Process (pid=" + Process.myPid() + ") lacks BLUETOOTH permission");
            return;
        }
        if (bluetoothState != State.UNINITIALIZED) {
            Log.w(TAG, "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, "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, "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(apprtcContext, bluetoothServiceListener, BluetoothProfile.HEADSET)) {
            Log.e(TAG, "BluetoothAdapter.getProfileProxy(HEADSET) failed");
            return;
        }
        // Register receivers for BluetoothHeadset change notifications.
        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, "HEADSET profile state: " + stateToString(bluetoothAdapter.getProfileConnectionState(BluetoothProfile.HEADSET)));
        Log.d(TAG, "Bluetooth proxy for headset profile has started");
        bluetoothState = State.HEADSET_UNAVAILABLE;
        Log.d(TAG, "start done: BT state=" + bluetoothState);
    }

    /**
     * Stops and closes all components related to Bluetooth audio.
     */
    public void stop() {
        ThreadUtils.checkIsOnMainThread();
        unregisterReceiver(bluetoothHeadsetReceiver);
        Log.d(TAG, "stop: BT state=" + bluetoothState);
        if (bluetoothAdapter != null) {
            // Stop BT SCO connection with remote device if needed.
            stopScoAudio();
            // Close down remaining BT resources.
            if (bluetoothState != State.UNINITIALIZED) {
                cancelTimer();
                if (bluetoothHeadset != null) {
                    bluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, bluetoothHeadset);
                    bluetoothHeadset = null;
                }
                bluetoothAdapter = null;
                bluetoothDevice = null;
                bluetoothState = State.UNINITIALIZED;
            }
        }
        Log.d(TAG, "stop done: BT state=" + bluetoothState);
    }

    /**
     * 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(TAG, "startSco: BT state=" + bluetoothState + ", " + "attempts: " + scoConnectionAttempts + ", " + "SCO is on: " + isSreplaced());
        if (scoConnectionAttempts >= MAX_SCO_CONNECTION_ATTEMPTS) {
            Log.e(TAG, "BT SCO connection fails - no more attempts");
            return false;
        }
        if (bluetoothState != State.HEADSET_AVAILABLE) {
            Log.e(TAG, "BT SCO connection fails - no headset available");
            return false;
        }
        // Start BT SCO channel and wait for ACTION_AUDIO_STATE_CHANGED.
        Log.d(TAG, "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.
        bluetoothState = State.SCO_CONNECTING;
        audioManager.startBluetoothSco();
        scoConnectionAttempts++;
        startTimer();
        Log.d(TAG, "startScoAudio done: BT state=" + bluetoothState);
        return true;
    }

    /**
     * Stops Bluetooth SCO connection with remote device.
     */
    public void stopScoAudio() {
        ThreadUtils.checkIsOnMainThread();
        Log.d(TAG, "stopScoAudio: BT state=" + bluetoothState + ", " + "SCO is on: " + isSreplaced());
        if (bluetoothState != State.SCO_CONNECTING && bluetoothState != State.SCO_CONNECTED) {
            return;
        }
        cancelTimer();
        audioManager.stopBluetoothSco();
        bluetoothState = State.SCO_DISCONNECTING;
        Log.d(TAG, "stopScoAudio done: BT state=" + bluetoothState);
    }

    /**
     * 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.
     */
    public void updateDevice() {
        if (bluetoothState == State.UNINITIALIZED || bluetoothHeadset == null) {
            return;
        }
        Log.d(TAG, "updateDevice");
        // Get connected devices for the headset profile. Returns the set of
        // devices which are in state STATE_CONNECTED. The BluetoothDevice clreplaced
        // is just a thin wrapper for a Bluetooth hardware address.
        List<BluetoothDevice> devices = bluetoothHeadset.getConnectedDevices();
        if (devices.isEmpty()) {
            bluetoothDevice = null;
            bluetoothState = State.HEADSET_UNAVAILABLE;
            Log.d(TAG, "No connected bluetooth headset");
        } else {
            // Always use first device is list. Android only supports one device.
            bluetoothDevice = devices.get(0);
            bluetoothState = State.HEADSET_AVAILABLE;
            Log.d(TAG, "Connected bluetooth headset: " + "name=" + bluetoothDevice.getName() + ", " + "state=" + stateToString(bluetoothHeadset.getConnectionState(bluetoothDevice)) + ", SCO audio=" + bluetoothHeadset.isAudioConnected(bluetoothDevice));
        }
        Log.d(TAG, "updateDevice done: BT state=" + bluetoothState);
    }

    /**
     * Stubs for test mocks.
     */
    protected AudioManager getAudioManager(Context context) {
        return (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
    }

    protected void registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
        apprtcContext.registerReceiver(receiver, filter);
    }

    protected void unregisterReceiver(BroadcastReceiver receiver) {
        apprtcContext.unregisterReceiver(receiver);
    }

    protected boolean getBluetoothProfileProxy(Context context, BluetoothProfile.ServiceListener listener, int profile) {
        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.
     */
    protected void logBluetoothAdapterInfo(BluetoothAdapter localAdapter) {
        Log.d(TAG, "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(TAG, "paired devices:");
            for (BluetoothDevice device : pairedDevices) {
                Log.d(TAG, " name=" + device.getName() + ", address=" + device.getAddress());
            }
        }
    }

    /**
     * Ensures that the audio manager updates its list of available audio devices.
     */
    private void updateAudioDeviceState() {
        ThreadUtils.checkIsOnMainThread();
        Log.d(TAG, "updateAudioDeviceState");
        apprtcAudioManager.updateAudioDeviceState();
    }

    /**
     * Starts timer which times out after BLUETOOTH_SCO_TIMEOUT_MS milliseconds.
     */
    private void startTimer() {
        ThreadUtils.checkIsOnMainThread();
        Log.d(TAG, "startTimer");
        handler.postDelayed(bluetoothTimeoutRunnable, BLUETOOTH_SCO_TIMEOUT_MS);
    }

    /**
     * Cancels any outstanding timer tasks.
     */
    private void cancelTimer() {
        ThreadUtils.checkIsOnMainThread();
        Log.d(TAG, "cancelTimer");
        handler.removeCallbacks(bluetoothTimeoutRunnable);
    }

    /**
     * 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() {
        ThreadUtils.checkIsOnMainThread();
        if (bluetoothState == State.UNINITIALIZED || bluetoothHeadset == null) {
            return;
        }
        Log.d(TAG, "bluetoothTimeout: BT state=" + bluetoothState + ", " + "attempts: " + scoConnectionAttempts + ", " + "SCO is on: " + isSreplaced());
        if (bluetoothState != State.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, "SCO connected with " + bluetoothDevice.getName());
                scoConnected = true;
            } else {
                Log.d(TAG, "SCO is not connected with " + bluetoothDevice.getName());
            }
        }
        if (scoConnected) {
            // We thought BT had timed out, but it's actually on; updating state.
            bluetoothState = State.SCO_CONNECTED;
            scoConnectionAttempts = 0;
        } else {
            // Give up and "cancel" our request by calling stopBluetoothSco().
            Log.w(TAG, "BT failed to connect after timeout");
            stopScoAudio();
        }
        updateAudioDeviceState();
        Log.d(TAG, "bluetoothTimeout done: BT state=" + bluetoothState);
    }

    /**
     * Checks whether audio uses Bluetooth SCO.
     */
    private boolean isSreplaced() {
        return audioManager.isBluetoothSreplaced();
    }

    /**
     * Converts BluetoothAdapter states into local string representations.
     */
    private String stateToString(int state) {
        switch(state) {
            case BluetoothAdapter.STATE_DISCONNECTED:
                return "DISCONNECTED";
            case BluetoothAdapter.STATE_CONNECTED:
                return "CONNECTED";
            case BluetoothAdapter.STATE_CONNECTING:
                return "CONNECTING";
            case BluetoothAdapter.STATE_DISCONNECTING:
                return "DISCONNECTING";
            case BluetoothAdapter.STATE_OFF:
                return "OFF";
            case BluetoothAdapter.STATE_ON:
                return "ON";
            case BluetoothAdapter.STATE_TURNING_OFF:
                // 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
                // for STATE_ON before attempting to use the adapter.
                return "TURNING_ON";
            default:
                return "INVALID";
        }
    }
}

17 Source : AppRTCBluetoothManager.java
with GNU General Public License v3.0
from snikket-im

/**
 * AppRTCProximitySensor manages functions related to Bluetoth devices in the
 * AppRTC demo.
 */
public clreplaced AppRTCBluetoothManager {

    // 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;

    private final Context apprtcContext;

    private final AppRTCAudioManager apprtcAudioManager;

    @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;

    // 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();
        }
    };

    protected AppRTCBluetoothManager(Context context, AppRTCAudioManager audioManager) {
        Log.d(Config.LOGTAG, "ctor");
        ThreadUtils.checkIsOnMainThread();
        apprtcContext = context;
        apprtcAudioManager = audioManager;
        this.audioManager = getAudioManager(context);
        bluetoothState = State.UNINITIALIZED;
        bluetoothServiceListener = new BluetoothServiceListener();
        bluetoothHeadsetReceiver = new BluetoothHeadsetBroadcastReceiver();
        handler = new Handler(Looper.getMainLooper());
    }

    /**
     * Construction.
     */
    static AppRTCBluetoothManager create(Context context, AppRTCAudioManager audioManager) {
        Log.d(Config.LOGTAG, "create" + AppRTCUtils.getThreadInfo());
        return new AppRTCBluetoothManager(context, audioManager);
    }

    /**
     * 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.
     */
    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;
        }
        bluetoothHeadset = null;
        bluetoothDevice = null;
        scoConnectionAttempts = 0;
        // Get a handle to the default local Bluetooth adapter.
        bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
        if (bluetoothAdapter == null) {
            Log.w(Config.LOGTAG, "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(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(apprtcContext, bluetoothServiceListener, BluetoothProfile.HEADSET)) {
            Log.e(Config.LOGTAG, "BluetoothAdapter.getProfileProxy(HEADSET) failed");
            return;
        }
        // Register receivers for BluetoothHeadset change notifications.
        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(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.
     */
    public void stop() {
        ThreadUtils.checkIsOnMainThread();
        Log.d(Config.LOGTAG, "stop: BT state=" + bluetoothState);
        if (bluetoothAdapter == null) {
            return;
        }
        // Stop BT SCO connection with remote device if needed.
        stopScoAudio();
        // Close down remaining BT resources.
        if (bluetoothState == State.UNINITIALIZED) {
            return;
        }
        unregisterReceiver(bluetoothHeadsetReceiver);
        cancelTimer();
        if (bluetoothHeadset != null) {
            bluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, bluetoothHeadset);
            bluetoothHeadset = null;
        }
        bluetoothAdapter = null;
        bluetoothDevice = null;
        bluetoothState = State.UNINITIALIZED;
        Log.d(Config.LOGTAG, "stop done: BT state=" + bluetoothState);
    }

    /**
     * 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: " + isSreplaced());
        if (scoConnectionAttempts >= MAX_SCO_CONNECTION_ATTEMPTS) {
            Log.e(Config.LOGTAG, "BT SCO connection fails - no more attempts");
            return false;
        }
        if (bluetoothState != State.HEADSET_AVAILABLE) {
            Log.e(Config.LOGTAG, "BT SCO connection fails - no headset available");
            return false;
        }
        // Start BT SCO channel and wait for ACTION_AUDIO_STATE_CHANGED.
        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.
        bluetoothState = State.SCO_CONNECTING;
        audioManager.startBluetoothSco();
        audioManager.setBluetoothSreplaced(true);
        scoConnectionAttempts++;
        startTimer();
        Log.d(Config.LOGTAG, "startScoAudio done: BT state=" + bluetoothState + ", " + "SCO is on: " + isSreplaced());
        return true;
    }

    /**
     * Stops Bluetooth SCO connection with remote device.
     */
    public void stopScoAudio() {
        ThreadUtils.checkIsOnMainThread();
        Log.d(Config.LOGTAG, "stopScoAudio: BT state=" + bluetoothState + ", " + "SCO is on: " + isSreplaced());
        if (bluetoothState != State.SCO_CONNECTING && bluetoothState != State.SCO_CONNECTED) {
            return;
        }
        cancelTimer();
        audioManager.stopBluetoothSco();
        audioManager.setBluetoothSreplaced(false);
        bluetoothState = State.SCO_DISCONNECTING;
        Log.d(Config.LOGTAG, "stopScoAudio done: BT state=" + bluetoothState + ", " + "SCO is on: " + isSreplaced());
    }

    /**
     * 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.
     */
    public void updateDevice() {
        if (bluetoothState == State.UNINITIALIZED || bluetoothHeadset == null) {
            return;
        }
        Log.d(Config.LOGTAG, "updateDevice");
        // Get connected devices for the headset profile. Returns the set of
        // devices which are in state STATE_CONNECTED. The BluetoothDevice clreplaced
        // is just a thin wrapper for a Bluetooth hardware address.
        List<BluetoothDevice> devices = bluetoothHeadset.getConnectedDevices();
        if (devices.isEmpty()) {
            bluetoothDevice = null;
            bluetoothState = State.HEADSET_UNAVAILABLE;
            Log.d(Config.LOGTAG, "No connected bluetooth headset");
        } else {
            // 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, "updateDevice done: BT state=" + bluetoothState);
    }

    /**
     * Stubs for test mocks.
     */
    @Nullable
    protected AudioManager getAudioManager(Context context) {
        return (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
    }

    protected void registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
        apprtcContext.registerReceiver(receiver, filter);
    }

    protected void unregisterReceiver(BroadcastReceiver receiver) {
        apprtcContext.unregisterReceiver(receiver);
    }

    protected boolean getBluetoothProfileProxy(Context context, BluetoothProfile.ServiceListener listener, int profile) {
        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());
            }
        }
    }

    /**
     * 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.
     */
    private void startTimer() {
        ThreadUtils.checkIsOnMainThread();
        Log.d(Config.LOGTAG, "startTimer");
        handler.postDelayed(bluetoothTimeoutRunnable, BLUETOOTH_SCO_TIMEOUT_MS);
    }

    /**
     * Cancels any outstanding timer tasks.
     */
    private void cancelTimer() {
        ThreadUtils.checkIsOnMainThread();
        Log.d(Config.LOGTAG, "cancelTimer");
        handler.removeCallbacks(bluetoothTimeoutRunnable);
    }

    /**
     * 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() {
        ThreadUtils.checkIsOnMainThread();
        if (bluetoothState == State.UNINITIALIZED || bluetoothHeadset == null) {
            return;
        }
        Log.d(Config.LOGTAG, "bluetoothTimeout: BT state=" + bluetoothState + ", " + "attempts: " + scoConnectionAttempts + ", " + "SCO is on: " + isSreplaced());
        if (bluetoothState != State.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(Config.LOGTAG, "SCO connected with " + bluetoothDevice.getName());
                scoConnected = true;
            } else {
                Log.d(Config.LOGTAG, "SCO is not connected with " + bluetoothDevice.getName());
            }
        }
        if (scoConnected) {
            // We thought BT had timed out, but it's actually on; updating state.
            bluetoothState = State.SCO_CONNECTED;
            scoConnectionAttempts = 0;
        } else {
            // Give up and "cancel" our request by calling stopBluetoothSco().
            Log.w(Config.LOGTAG, "BT failed to connect after timeout");
            stopScoAudio();
        }
        updateAudioDeviceState();
        Log.d(Config.LOGTAG, "bluetoothTimeout done: BT state=" + bluetoothState);
    }

    /**
     * Checks whether audio uses Bluetooth SCO.
     */
    private boolean isSreplaced() {
        return audioManager.isBluetoothSreplaced();
    }

    /**
     * Converts BluetoothAdapter states into local string representations.
     */
    private String stateToString(int state) {
        switch(state) {
            case BluetoothAdapter.STATE_DISCONNECTED:
                return "DISCONNECTED";
            case BluetoothAdapter.STATE_CONNECTED:
                return "CONNECTED";
            case BluetoothAdapter.STATE_CONNECTING:
                return "CONNECTING";
            case BluetoothAdapter.STATE_DISCONNECTING:
                return "DISCONNECTING";
            case BluetoothAdapter.STATE_OFF:
                return "OFF";
            case BluetoothAdapter.STATE_ON:
                return "ON";
            case BluetoothAdapter.STATE_TURNING_OFF:
                // 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
                // for STATE_ON before attempting to use the adapter.
                return "TURNING_ON";
            default:
                return "INVALID";
        }
    }

    // Bluetooth connection state.
    public enum State {

        // Bluetooth is not available; no adapter or Bluetooth is off.
        UNINITIALIZED,
        // Bluetooth error happened when trying to start Bluetooth.
        ERROR,
        // Bluetooth proxy object for the Headset profile exists, but no connected headset devices,
        // SCO is not started or disconnected.
        HEADSET_UNAVAILABLE,
        // Bluetooth proxy object for the Headset profile 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
    }

    /**
     * Implementation of an interface that notifies BluetoothProfile IPC clients when they have been
     * connected to or disconnected from the service.
     */
    private clreplaced BluetoothServiceListener implements BluetoothProfile.ServiceListener {

        @Override
        public // connection and perform other operations that are relevant to the headset profile.
        void onServiceConnected(int profile, BluetoothProfile proxy) {
            if (profile != BluetoothProfile.HEADSET || bluetoothState == State.UNINITIALIZED) {
                return;
            }
            Log.d(Config.LOGTAG, "BluetoothServiceListener.onServiceConnected: BT state=" + bluetoothState);
            // Android only supports one connected Bluetooth Headset at a time.
            bluetoothHeadset = (BluetoothHeadset) proxy;
            updateAudioDeviceState();
            Log.d(Config.LOGTAG, "onServiceConnected done: BT state=" + bluetoothState);
        }

        @Override
        public /**
         * Notifies the client when the proxy object has been disconnected from the service.
         */
        void onServiceDisconnected(int profile) {
            if (profile != BluetoothProfile.HEADSET || bluetoothState == State.UNINITIALIZED) {
                return;
            }
            Log.d(Config.LOGTAG, "BluetoothServiceListener.onServiceDisconnected: BT state=" + bluetoothState);
            stopScoAudio();
            bluetoothHeadset = null;
            bluetoothDevice = null;
            bluetoothState = State.HEADSET_UNAVAILABLE;
            updateAudioDeviceState();
            Log.d(Config.LOGTAG, "onServiceDisconnected done: BT state=" + bluetoothState);
        }
    }

    // Intent broadcast receiver which handles changes in Bluetooth device availability.
    // Detects headset changes and Bluetooth SCO state changes.
    private clreplaced BluetoothHeadsetBroadcastReceiver extends BroadcastReceiver {

        @Override
        public void onReceive(Context context, Intent intent) {
            if (bluetoothState == State.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. Typically received when user turns on a BT
            // 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);
                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.
                    stopScoAudio();
                    updateAudioDeviceState();
                }
            // 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);
                if (state == BluetoothHeadset.STATE_AUDIO_CONNECTED) {
                    cancelTimer();
                    if (bluetoothState == State.SCO_CONNECTING) {
                        Log.d(Config.LOGTAG, "+++ Bluetooth audio SCO is now connected");
                        bluetoothState = State.SCO_CONNECTED;
                        scoConnectionAttempts = 0;
                        updateAudioDeviceState();
                    } else {
                        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.");
                        return;
                    }
                    updateAudioDeviceState();
                }
            }
            Log.d(Config.LOGTAG, "onReceive done: BT state=" + bluetoothState);
        }
    }
}

17 Source : AppRTCBluetoothManager.java
with MIT License
from qunarcorp

/**
 * AppRTCProximitySensor manages functions related to Bluetoth devices in the
 * AppRTC demo.
 */
public clreplaced AppRTCBluetoothManager {

    private static final String TAG = "AppRTCBluetoothManager";

    // 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;

    // Bluetooth connection state.
    public enum State {

        // Bluetooth is not available; no adapter or Bluetooth is off.
        UNINITIALIZED,
        // Bluetooth error happened when trying to start Bluetooth.
        ERROR,
        // Bluetooth proxy object for the Headset profile exists, but no connected headset devices,
        // SCO is not started or disconnected.
        HEADSET_UNAVAILABLE,
        // Bluetooth proxy object for the Headset profile 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
    }

    private final Context apprtcContext;

    private final AppRTCAudioManager apprtcAudioManager;

    private final AudioManager audioManager;

    private final Handler handler;

    int scoConnectionAttempts;

    private State bluetoothState;

    private final BluetoothProfile.ServiceListener bluetoothServiceListener;

    private BluetoothAdapter bluetoothAdapter;

    private BluetoothHeadset bluetoothHeadset;

    private BluetoothDevice bluetoothDevice;

    private final BroadcastReceiver bluetoothHeadsetReceiver;

    // 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();
        }
    };

    /**
     * Implementation of an interface that notifies BluetoothProfile IPC clients when they have been
     * connected to or disconnected from the service.
     */
    private clreplaced BluetoothServiceListener implements BluetoothProfile.ServiceListener {

        @Override
        public // connection and perform other operations that are relevant to the headset profile.
        void onServiceConnected(int profile, BluetoothProfile proxy) {
            if (profile != BluetoothProfile.HEADSET || bluetoothState == State.UNINITIALIZED) {
                return;
            }
            LogUtil.d(TAG, "BluetoothServiceListener.onServiceConnected: BT state=" + bluetoothState);
            // Android only supports one connected Bluetooth Headset at a time.
            bluetoothHeadset = (BluetoothHeadset) proxy;
            updateAudioDeviceState();
            LogUtil.d(TAG, "onServiceConnected done: BT state=" + bluetoothState);
        }

        @Override
        public /**
         * Notifies the client when the proxy object has been disconnected from the service.
         */
        void onServiceDisconnected(int profile) {
            if (profile != BluetoothProfile.HEADSET || bluetoothState == State.UNINITIALIZED) {
                return;
            }
            LogUtil.d(TAG, "BluetoothServiceListener.onServiceDisconnected: BT state=" + bluetoothState);
            stopScoAudio();
            bluetoothHeadset = null;
            bluetoothDevice = null;
            bluetoothState = State.HEADSET_UNAVAILABLE;
            updateAudioDeviceState();
            LogUtil.d(TAG, "onServiceDisconnected done: BT state=" + bluetoothState);
        }
    }

    // Intent broadcast receiver which handles changes in Bluetooth device availability.
    // Detects headset changes and Bluetooth SCO state changes.
    private clreplaced BluetoothHeadsetBroadcastReceiver extends BroadcastReceiver {

        @Override
        public void onReceive(Context context, Intent intent) {
            if (bluetoothState == State.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. Typically received when user turns on a BT
            // 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);
                LogUtil.d(TAG, "BluetoothHeadsetBroadcastReceiver.onReceive: " + "a=ACTION_CONNECTION_STATE_CHANGED, " + "s=" + stateToString(state) + ", " + "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.
                    stopScoAudio();
                    updateAudioDeviceState();
                }
            // 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);
                LogUtil.d(TAG, "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) {
                        LogUtil.d(TAG, "+++ Bluetooth audio SCO is now connected");
                        bluetoothState = State.SCO_CONNECTED;
                        scoConnectionAttempts = 0;
                        updateAudioDeviceState();
                    } else {
                        Log.w(TAG, "Unexpected state BluetoothHeadset.STATE_AUDIO_CONNECTED");
                    }
                } else if (state == BluetoothHeadset.STATE_AUDIO_CONNECTING) {
                    LogUtil.d(TAG, "+++ Bluetooth audio SCO is now connecting...");
                } else if (state == BluetoothHeadset.STATE_AUDIO_DISCONNECTED) {
                    LogUtil.d(TAG, "+++ Bluetooth audio SCO is now disconnected");
                    if (isInitialStickyBroadcast()) {
                        LogUtil.d(TAG, "Ignore STATE_AUDIO_DISCONNECTED initial sticky broadcast.");
                        return;
                    }
                    updateAudioDeviceState();
                }
            }
            LogUtil.d(TAG, "onReceive done: BT state=" + bluetoothState);
        }
    }

    /**
     * Construction.
     */
    static AppRTCBluetoothManager create(Context context, AppRTCAudioManager audioManager) {
        LogUtil.d(TAG, "create" + AppRTCUtils.getThreadInfo());
        return new AppRTCBluetoothManager(context, audioManager);
    }

    protected AppRTCBluetoothManager(Context context, AppRTCAudioManager audioManager) {
        LogUtil.d(TAG, "ctor");
        apprtcContext = context;
        AppRTCUtils.checkIsOnMainThread(apprtcContext);
        apprtcAudioManager = audioManager;
        this.audioManager = getAudioManager(context);
        bluetoothState = State.UNINITIALIZED;
        bluetoothServiceListener = new BluetoothServiceListener();
        bluetoothHeadsetReceiver = new BluetoothHeadsetBroadcastReceiver();
        handler = new Handler(Looper.getMainLooper());
    }

    /**
     * Returns the internal state.
     */
    public State getState() {
        AppRTCUtils.checkIsOnMainThread(apprtcContext);
        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.
     */
    public void start() {
        AppRTCUtils.checkIsOnMainThread(apprtcContext);
        LogUtil.d(TAG, "start");
        if (!hasPermission(apprtcContext, android.Manifest.permission.BLUETOOTH)) {
            Log.w(TAG, "Process (pid=" + Process.myPid() + ") lacks BLUETOOTH permission");
            return;
        }
        if (bluetoothState != State.UNINITIALIZED) {
            Log.w(TAG, "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, "Device does not support Bluetooth");
            return;
        }
        // Ensure that the device supports use of BT SCO audio for off call use cases.
        if (!audioManager.isBluetoothScoAvailableOffCall()) {
            LogUtil.e(TAG, "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(apprtcContext, bluetoothServiceListener, BluetoothProfile.HEADSET)) {
            LogUtil.e(TAG, "BluetoothAdapter.getProfileProxy(HEADSET) failed");
            return;
        }
        // Register receivers for BluetoothHeadset change notifications.
        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);
        try {
            registerReceiver(bluetoothHeadsetReceiver, bluetoothHeadsetFilter);
        } catch (Exception ex) {
            LogUtil.e(ex.getMessage());
        }
        LogUtil.d(TAG, "HEADSET profile state: " + stateToString(bluetoothAdapter.getProfileConnectionState(BluetoothProfile.HEADSET)));
        LogUtil.d(TAG, "Bluetooth proxy for headset profile has started");
        bluetoothState = State.HEADSET_UNAVAILABLE;
        LogUtil.d(TAG, "start done: BT state=" + bluetoothState);
    }

    /**
     * Stops and closes all components related to Bluetooth audio.
     */
    public void stop() {
        AppRTCUtils.checkIsOnMainThread(apprtcContext);
        try {
            unregisterReceiver(bluetoothHeadsetReceiver);
        } catch (Exception ex) {
            LogUtil.e(ex.getMessage());
        }
        LogUtil.d(TAG, "stop: BT state=" + bluetoothState);
        if (bluetoothAdapter != null) {
            // Stop BT SCO connection with remote device if needed.
            stopScoAudio();
            // Close down remaining BT resources.
            if (bluetoothState != State.UNINITIALIZED) {
                cancelTimer();
                if (bluetoothHeadset != null) {
                    bluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, bluetoothHeadset);
                    bluetoothHeadset = null;
                }
                bluetoothAdapter = null;
                bluetoothDevice = null;
                bluetoothState = State.UNINITIALIZED;
            }
        }
        LogUtil.d(TAG, "stop done: BT state=" + bluetoothState);
    }

    /**
     * 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() {
        AppRTCUtils.checkIsOnMainThread(apprtcContext);
        LogUtil.d(TAG, "startSco: BT state=" + bluetoothState + ", " + "attempts: " + scoConnectionAttempts + ", " + "SCO is on: " + isSreplaced());
        if (scoConnectionAttempts >= MAX_SCO_CONNECTION_ATTEMPTS) {
            LogUtil.e(TAG, "BT SCO connection fails - no more attempts");
            return false;
        }
        if (bluetoothState != State.HEADSET_AVAILABLE) {
            LogUtil.e(TAG, "BT SCO connection fails - no headset available");
            return false;
        }
        // Start BT SCO channel and wait for ACTION_AUDIO_STATE_CHANGED.
        LogUtil.d(TAG, "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.
        bluetoothState = State.SCO_CONNECTING;
        audioManager.startBluetoothSco();
        scoConnectionAttempts++;
        startTimer();
        LogUtil.d(TAG, "startScoAudio done: BT state=" + bluetoothState);
        return true;
    }

    /**
     * Stops Bluetooth SCO connection with remote device.
     */
    public void stopScoAudio() {
        AppRTCUtils.checkIsOnMainThread(apprtcContext);
        LogUtil.d(TAG, "stopScoAudio: BT state=" + bluetoothState + ", " + "SCO is on: " + isSreplaced());
        if (bluetoothState != State.SCO_CONNECTING && bluetoothState != State.SCO_CONNECTED) {
            return;
        }
        cancelTimer();
        audioManager.stopBluetoothSco();
        bluetoothState = State.SCO_DISCONNECTING;
        LogUtil.d(TAG, "stopScoAudio done: BT state=" + bluetoothState);
    }

    /**
     * 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.
     */
    public void updateDevice() {
        if (bluetoothState == State.UNINITIALIZED || bluetoothHeadset == null) {
            return;
        }
        LogUtil.d(TAG, "updateDevice");
        // Get connected devices for the headset profile. Returns the set of
        // devices which are in state STATE_CONNECTED. The BluetoothDevice clreplaced
        // is just a thin wrapper for a Bluetooth hardware address.
        List<BluetoothDevice> devices = bluetoothHeadset.getConnectedDevices();
        if (devices.isEmpty()) {
            bluetoothDevice = null;
            bluetoothState = State.HEADSET_UNAVAILABLE;
            LogUtil.d(TAG, "No connected bluetooth headset");
        } else {
            // Always use first device is list. Android only supports one device.
            bluetoothDevice = devices.get(0);
            bluetoothState = State.HEADSET_AVAILABLE;
            LogUtil.d(TAG, "Connected bluetooth headset: " + "name=" + bluetoothDevice.getName() + ", " + "state=" + stateToString(bluetoothHeadset.getConnectionState(bluetoothDevice)) + ", SCO audio=" + bluetoothHeadset.isAudioConnected(bluetoothDevice));
        }
        LogUtil.d(TAG, "updateDevice done: BT state=" + bluetoothState);
    }

    /**
     * Stubs for test mocks.
     */
    protected AudioManager getAudioManager(Context context) {
        return (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
    }

    protected void registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
        apprtcContext.registerReceiver(receiver, filter);
    }

    protected void unregisterReceiver(BroadcastReceiver receiver) {
        apprtcContext.unregisterReceiver(receiver);
    }

    protected boolean getBluetoothProfileProxy(Context context, BluetoothProfile.ServiceListener listener, int profile) {
        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.
     */
    protected void logBluetoothAdapterInfo(BluetoothAdapter localAdapter) {
        LogUtil.d(TAG, "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()) {
            LogUtil.d(TAG, "paired devices:");
            for (BluetoothDevice device : pairedDevices) {
                LogUtil.d(TAG, " name=" + device.getName() + ", address=" + device.getAddress());
            }
        }
    }

    /**
     * Ensures that the audio manager updates its list of available audio devices.
     */
    private void updateAudioDeviceState() {
        AppRTCUtils.checkIsOnMainThread(apprtcContext);
        LogUtil.d(TAG, "updateAudioDeviceState");
        apprtcAudioManager.updateAudioDeviceState();
    }

    /**
     * Starts timer which times out after BLUETOOTH_SCO_TIMEOUT_MS milliseconds.
     */
    private void startTimer() {
        AppRTCUtils.checkIsOnMainThread(apprtcContext);
        LogUtil.d(TAG, "startTimer");
        handler.postDelayed(bluetoothTimeoutRunnable, BLUETOOTH_SCO_TIMEOUT_MS);
    }

    /**
     * Cancels any outstanding timer tasks.
     */
    private void cancelTimer() {
        AppRTCUtils.checkIsOnMainThread(apprtcContext);
        LogUtil.d(TAG, "cancelTimer");
        handler.removeCallbacks(bluetoothTimeoutRunnable);
    }

    /**
     * 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() {
        AppRTCUtils.checkIsOnMainThread(apprtcContext);
        if (bluetoothState == State.UNINITIALIZED || bluetoothHeadset == null) {
            return;
        }
        LogUtil.d(TAG, "bluetoothTimeout: BT state=" + bluetoothState + ", " + "attempts: " + scoConnectionAttempts + ", " + "SCO is on: " + isSreplaced());
        if (bluetoothState != State.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)) {
                LogUtil.d(TAG, "SCO connected with " + bluetoothDevice.getName());
                scoConnected = true;
            } else {
                LogUtil.d(TAG, "SCO is not connected with " + bluetoothDevice.getName());
            }
        }
        if (scoConnected) {
            // We thought BT had timed out, but it's actually on; updating state.
            bluetoothState = State.SCO_CONNECTED;
            scoConnectionAttempts = 0;
        } else {
            // Give up and "cancel" our request by calling stopBluetoothSco().
            Log.w(TAG, "BT failed to connect after timeout");
            stopScoAudio();
        }
        updateAudioDeviceState();
        LogUtil.d(TAG, "bluetoothTimeout done: BT state=" + bluetoothState);
    }

    /**
     * Checks whether audio uses Bluetooth SCO.
     */
    private boolean isSreplaced() {
        return audioManager.isBluetoothSreplaced();
    }

    /**
     * Converts BluetoothAdapter states into local string representations.
     */
    private String stateToString(int state) {
        switch(state) {
            case BluetoothAdapter.STATE_DISCONNECTED:
                return "DISCONNECTED";
            case BluetoothAdapter.STATE_CONNECTED:
                return "CONNECTED";
            case BluetoothAdapter.STATE_CONNECTING:
                return "CONNECTING";
            case BluetoothAdapter.STATE_DISCONNECTING:
                return "DISCONNECTING";
            case BluetoothAdapter.STATE_OFF:
                return "OFF";
            case BluetoothAdapter.STATE_ON:
                return "ON";
            case BluetoothAdapter.STATE_TURNING_OFF:
                // 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
                // for STATE_ON before attempting to use the adapter.
                return "TURNING_ON";
            default:
                return "INVALID";
        }
    }
}

17 Source : BluetoothHeadsetUtils.java
with Apache License 2.0
from niedev

/**
 * This is a utility to detect bluetooth headset connection and establish audio connection
 * for android API >= 8. This includes a work around for  API < 11 to detect already connected headset
 * before the application starts. This work around would only fails if Sco audio
 * connection is accepted but the connected device is not a headset.
 *
 * @author Hoan Nguyen
 */
public abstract clreplaced BluetoothHeadsetUtils {

    private Context mContext;

    private BluetoothAdapter mBluetoothAdapter;

    private BluetoothHeadset mBluetoothHeadset;

    private BluetoothDevice mConnectedHeadset;

    private AudioManager mAudioManager;

    private boolean mIsCountDownOn;

    private boolean mIsStarting;

    private boolean mIsOnHeadsetSco;

    private boolean mIsStarted;

    // $NON-NLS-1$
    private static final String TAG = "BluetoothHeadsetUtils";

    /**
     * Constructor
     *
     * @param context
     */
    public BluetoothHeadsetUtils(Context context) {
        mContext = context;
        mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
        mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
    }

    /**
     * Call this to start BluetoothHeadsetUtils functionalities.
     *
     * @return The return value of startBluetooth() or startBluetooth11()
     */
    public boolean start() {
        if (!mIsStarted) {
            mIsStarted = startBluetooth11();
        }
        return mIsStarted;
    }

    /**
     * Should call this on onResume or onDestroy.
     * Unregister broadcast receivers and stop Sco audio connection
     * and cancel count down.
     */
    public void stop() {
        if (mIsStarted) {
            mIsStarted = false;
            stopBluetooth11();
        }
    }

    /**
     * @return true if audio is connected through headset.
     */
    public boolean isOnHeadsetSco() {
        return mIsOnHeadsetSco;
    }

    public abstract void onHeadsetDisconnected();

    public abstract void onHeadsetConnected();

    public abstract void onScoAudioDisconnected();

    public abstract void onScoAudioConnected();

    /**
     * Register a headset profile listener
     *
     * @return false    if device does not support bluetooth or current platform does not supports
     * use of SCO for off call or error in getting profile proxy.
     */
    private boolean startBluetooth11() {
        // //d(TAG, "startBluetooth11"); //$NON-NLS-1$
        // Device support bluetooth
        if (mBluetoothAdapter != null) {
            if (mAudioManager.isBluetoothScoAvailableOffCall()) {
                // All the detection and audio connection are done in mHeadsetProfileListener
                return mBluetoothAdapter.getProfileProxy(mContext, mHeadsetProfileListener, BluetoothProfile.HEADSET);
            }
        }
        return false;
    }

    /**
     * Unregister broadcast receivers and stop Sco audio connection
     * and cancel count down.
     */
    protected void stopBluetooth11() {
        // //d(TAG, "stopBluetooth11"); //$NON-NLS-1$
        if (mIsCountDownOn) {
            mIsCountDownOn = false;
            mCountDown11.cancel();
        }
        if (mBluetoothHeadset != null) {
            // Need to call stopVoiceRecognition here when the app
            // change orientation or close with headset still turns on.
            mBluetoothHeadset.stopVoiceRecognition(mConnectedHeadset);
            try {
                mContext.unregisterReceiver(mHeadsetBroadcastReceiver);
            } catch (Exception e) {
            }
            mBluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, mBluetoothHeadset);
            mBluetoothHeadset = null;
        }
    }

    /**
     * Check for already connected headset and if so start audio connection.
     * Register for broadcast of headset and Sco audio connection states.
     */
    private BluetoothProfile.ServiceListener mHeadsetProfileListener = new BluetoothProfile.ServiceListener() {

        /**
         * This method is never called, even when we closeProfileProxy on onPause.
         * When or will it ever be called???
         */
        @Override
        public void onServiceDisconnected(int profile) {
            // //d(TAG, "Profile listener onServiceDisconnected"); //$NON-NLS-1$
            stopBluetooth11();
        }

        // @SuppressWarnings("synthetic-access")
        @Override
        public void onServiceConnected(int profile, BluetoothProfile proxy) {
            // //d(TAG, "Profile listener onServiceConnected"); //$NON-NLS-1$
            // mBluetoothHeadset is just a headset profile,
            // it does not represent a headset device.
            mBluetoothHeadset = (BluetoothHeadset) proxy;
            // If a headset is connected before this application starts,
            // ACTION_CONNECTION_STATE_CHANGED will not be broadcast.
            // So we need to check for already connected headset.
            List<BluetoothDevice> devices = mBluetoothHeadset.getConnectedDevices();
            if (devices.size() > 0) {
                // Only one headset can be connected at a time,
                // so the connected headset is at index 0.
                connectScoAudio(devices.get(0));
            // //d(TAG, "Start count down"); //$NON-NLS-1$
            }
            IntentFilter intentFilter = new IntentFilter();
            // During the active life time of the app, a user may turn on and off the headset.
            // So register for broadcast of connection states.
            intentFilter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
            // Calling startVoiceRecognition does not result in immediate audio connection.
            // So register for broadcast of audio connection states. This broadcast will
            // only be sent if startVoiceRecognition returns true.
            intentFilter.addAction(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED);
            mContext.registerReceiver(mHeadsetBroadcastReceiver, intentFilter);
        }
    };

    private void connectScoAudio(BluetoothDevice bluetoothDevice) {
        mConnectedHeadset = bluetoothDevice;
        onHeadsetConnected();
        // Should not need count down timer, but just in case.
        // See comment below in mHeadsetBroadcastReceiver onReceive()
        mIsCountDownOn = true;
        mCountDown11.start();
    }

    /**
     * Handle headset and Sco audio connection states.
     */
    private BroadcastReceiver mHeadsetBroadcastReceiver = new BroadcastReceiver() {

        @SuppressWarnings("synthetic-access")
        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            int state;
            if (BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED.equals(action)) {
                state = intent.getIntExtra(BluetoothHeadset.EXTRA_STATE, BluetoothHeadset.STATE_DISCONNECTED);
                // //d(TAG, "\nAction = " + action + "\nState = " + state); //$NON-NLS-1$ //$NON-NLS-2$
                if (state == BluetoothHeadset.STATE_CONNECTED) {
                    connectScoAudio((BluetoothDevice) intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE));
                // //d(TAG, "Start count down"); //$NON-NLS-1$
                } else if (state == BluetoothHeadset.STATE_DISCONNECTED) {
                    // Calling stopVoiceRecognition always returns false here
                    // as it should since the headset is no longer connected.
                    if (mIsCountDownOn) {
                        mIsCountDownOn = false;
                        mCountDown11.cancel();
                    }
                    mConnectedHeadset = null;
                    // override this if you want to do other thing when the device is disconnected.
                    onHeadsetDisconnected();
                // //d(TAG, "Headset disconnected"); //$NON-NLS-1$
                }
            } else // audio
            {
                state = intent.getIntExtra(BluetoothHeadset.EXTRA_STATE, BluetoothHeadset.STATE_AUDIO_DISCONNECTED);
                // //d(TAG, "\nAction = " + action + "\nState = " + state); //$NON-NLS-1$ //$NON-NLS-2$
                if (state == BluetoothHeadset.STATE_AUDIO_CONNECTED) {
                    // //d(TAG, "\nHeadset audio connected");  //$NON-NLS-1$
                    mIsOnHeadsetSco = true;
                    if (mIsCountDownOn) {
                        mIsCountDownOn = false;
                        mCountDown11.cancel();
                    }
                    // override this if you want to do other thing when headset audio is connected.
                    onScoAudioConnected();
                } else if (state == BluetoothHeadset.STATE_AUDIO_DISCONNECTED) {
                    mIsOnHeadsetSco = false;
                    // The headset audio is disconnected, but calling
                    // stopVoiceRecognition always returns true here.
                    mBluetoothHeadset.stopVoiceRecognition(mConnectedHeadset);
                    // override this if you want to do other thing when headset audio is disconnected.
                    onScoAudioDisconnected();
                // //d(TAG, "Headset audio disconnected"); //$NON-NLS-1$
                }
            }
        }
    };

    /**
     * Try to connect to audio headset in onTick.
     */
    private CustomCountDownTimer mCountDown11 = new CustomCountDownTimer(10000, 1000) {

        @SuppressWarnings("synthetic-access")
        @Override
        public void onTick(long millisUntilFinished) {
            // First stick calls always returns false. The second stick
            // always returns true if the countDownInterval is setSender to 1000.
            // It is somewhere in between 500 to a 1000.
            mBluetoothHeadset.startVoiceRecognition(mConnectedHeadset);
        // //d(TAG, "onTick startVoiceRecognition"); //$NON-NLS-1$
        }

        @SuppressWarnings("synthetic-access")
        @Override
        public void onFinish() {
            // Calls to startVoiceRecognition in onStick are not successful.
            // Should implement something to inform user of this failure
            mIsCountDownOn = false;
        // //d(TAG, "\nonFinish fail to connect to headset audio"); //$NON-NLS-1$
        }
    };

    public boolean isBluetoothScoAvailable() {
        return mAudioManager.isBluetoothSreplaced();
    }

    public boolean isHeadsetConnected() {
        if (mBluetoothHeadset == null || mBluetoothHeadset.getConnectedDevices().size() == 0) {
            return false;
        } else {
            return true;
        }
    }
}

17 Source : BluetoothManager.java
with BSD 3-Clause "New" or "Revised" License
from nhancv

/**
 * AppRTCProximitySensor manages functions related to Bluetoth devices in the
 * AppRTC demo.
 */
public clreplaced BluetoothManager {

    private static final String TAG = "AppRTCBluetoothManager";

    // 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;

    private final Context apprtcContext;

    private final RTCAudioManager apprtcAudioManager;

    private final android.media.AudioManager audioManager;

    private final Handler handler;

    private final BluetoothProfile.ServiceListener bluetoothServiceListener;

    private final BroadcastReceiver bluetoothHeadsetReceiver;

    private int scoConnectionAttempts;

    private State bluetoothState;

    private BluetoothAdapter bluetoothAdapter;

    private BluetoothHeadset bluetoothHeadset;

    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 BluetoothManager(Context context, RTCAudioManager audioManager) {
        Log.d(TAG, "ctor");
        ThreadUtils.checkIsOnMainThread();
        apprtcContext = context;
        apprtcAudioManager = audioManager;
        this.audioManager = getAudioManager(context);
        bluetoothState = State.UNINITIALIZED;
        bluetoothServiceListener = new BluetoothServiceListener();
        bluetoothHeadsetReceiver = new BluetoothHeadsetBroadcastReceiver();
        handler = new Handler(Looper.getMainLooper());
    }

    /**
     * Construction.
     */
    static BluetoothManager create(Context context, RTCAudioManager audioManager) {
        Log.d(TAG, "create" + Utils.getThreadInfo());
        return new BluetoothManager(context, audioManager);
    }

    /**
     * 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.
     */
    public void start() {
        ThreadUtils.checkIsOnMainThread();
        Log.d(TAG, "start");
        if (!hasPermission(apprtcContext, android.Manifest.permission.BLUETOOTH)) {
            Log.w(TAG, "Process (pid=" + Process.myPid() + ") lacks BLUETOOTH permission");
            return;
        }
        if (bluetoothState != State.UNINITIALIZED) {
            Log.w(TAG, "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, "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, "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(apprtcContext, bluetoothServiceListener, BluetoothProfile.HEADSET)) {
            Log.e(TAG, "BluetoothAdapter.getProfileProxy(HEADSET) failed");
            return;
        }
        // Register receivers for BluetoothHeadset change notifications.
        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, "HEADSET profile state: " + stateToString(bluetoothAdapter.getProfileConnectionState(BluetoothProfile.HEADSET)));
        Log.d(TAG, "Bluetooth proxy for headset profile has started");
        bluetoothState = State.HEADSET_UNAVAILABLE;
        Log.d(TAG, "start done: BT state=" + bluetoothState);
    }

    /**
     * Stops and closes all components related to Bluetooth audio.
     */
    public void stop() {
        ThreadUtils.checkIsOnMainThread();
        Log.d(TAG, "stop: BT state=" + bluetoothState);
        if (bluetoothAdapter == null) {
            return;
        }
        // Stop BT SCO connection with remote device if needed.
        stopScoAudio();
        // Close down remaining BT resources.
        if (bluetoothState == State.UNINITIALIZED) {
            return;
        }
        unregisterReceiver(bluetoothHeadsetReceiver);
        cancelTimer();
        if (bluetoothHeadset != null) {
            bluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, bluetoothHeadset);
            bluetoothHeadset = null;
        }
        bluetoothAdapter = null;
        bluetoothDevice = null;
        bluetoothState = State.UNINITIALIZED;
        Log.d(TAG, "stop done: BT state=" + bluetoothState);
    }

    /**
     * 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(TAG, "startSco: BT state=" + bluetoothState + ", " + "attempts: " + scoConnectionAttempts + ", " + "SCO is on: " + isSreplaced());
        if (scoConnectionAttempts >= MAX_SCO_CONNECTION_ATTEMPTS) {
            Log.e(TAG, "BT SCO connection fails - no more attempts");
            return false;
        }
        if (bluetoothState != State.HEADSET_AVAILABLE) {
            Log.e(TAG, "BT SCO connection fails - no headset available");
            return false;
        }
        // Start BT SCO channel and wait for ACTION_AUDIO_STATE_CHANGED.
        Log.d(TAG, "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.
        bluetoothState = State.SCO_CONNECTING;
        audioManager.startBluetoothSco();
        audioManager.setBluetoothSreplaced(true);
        scoConnectionAttempts++;
        startTimer();
        Log.d(TAG, "startScoAudio done: BT state=" + bluetoothState + ", " + "SCO is on: " + isSreplaced());
        return true;
    }

    /**
     * Stops Bluetooth SCO connection with remote device.
     */
    public void stopScoAudio() {
        ThreadUtils.checkIsOnMainThread();
        Log.d(TAG, "stopScoAudio: BT state=" + bluetoothState + ", " + "SCO is on: " + isSreplaced());
        if (bluetoothState != State.SCO_CONNECTING && bluetoothState != State.SCO_CONNECTED) {
            return;
        }
        cancelTimer();
        audioManager.stopBluetoothSco();
        audioManager.setBluetoothSreplaced(false);
        bluetoothState = State.SCO_DISCONNECTING;
        Log.d(TAG, "stopScoAudio done: BT state=" + bluetoothState + ", " + "SCO is on: " + isSreplaced());
    }

    /**
     * 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.
     */
    public void updateDevice() {
        if (bluetoothState == State.UNINITIALIZED || bluetoothHeadset == null) {
            return;
        }
        Log.d(TAG, "updateDevice");
        // Get connected devices for the headset profile. Returns the set of
        // devices which are in state STATE_CONNECTED. The BluetoothDevice clreplaced
        // is just a thin wrapper for a Bluetooth hardware address.
        List<BluetoothDevice> devices = bluetoothHeadset.getConnectedDevices();
        if (devices.isEmpty()) {
            bluetoothDevice = null;
            bluetoothState = State.HEADSET_UNAVAILABLE;
            Log.d(TAG, "No connected bluetooth headset");
        } else {
            // Always use first device in list. Android only supports one device.
            bluetoothDevice = devices.get(0);
            bluetoothState = State.HEADSET_AVAILABLE;
            Log.d(TAG, "Connected bluetooth headset: " + "name=" + bluetoothDevice.getName() + ", " + "state=" + stateToString(bluetoothHeadset.getConnectionState(bluetoothDevice)) + ", SCO audio=" + bluetoothHeadset.isAudioConnected(bluetoothDevice));
        }
        Log.d(TAG, "updateDevice done: BT state=" + bluetoothState);
    }

    /**
     * Stubs for test mocks.
     */
    protected android.media.AudioManager getAudioManager(Context context) {
        return (android.media.AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
    }

    protected void registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
        apprtcContext.registerReceiver(receiver, filter);
    }

    protected void unregisterReceiver(BroadcastReceiver receiver) {
        apprtcContext.unregisterReceiver(receiver);
    }

    protected boolean getBluetoothProfileProxy(Context context, BluetoothProfile.ServiceListener listener, int profile) {
        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(TAG, "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(TAG, "paired devices:");
            for (BluetoothDevice device : pairedDevices) {
                Log.d(TAG, " name=" + device.getName() + ", address=" + device.getAddress());
            }
        }
    }

    /**
     * Ensures that the audio manager updates its list of available audio devices.
     */
    private void updateAudioDeviceState() {
        ThreadUtils.checkIsOnMainThread();
        Log.d(TAG, "updateAudioDeviceState");
        apprtcAudioManager.updateAudioDeviceState();
    }

    /**
     * Starts timer which times out after BLUETOOTH_SCO_TIMEOUT_MS milliseconds.
     */
    private void startTimer() {
        ThreadUtils.checkIsOnMainThread();
        Log.d(TAG, "startTimer");
        handler.postDelayed(bluetoothTimeoutRunnable, BLUETOOTH_SCO_TIMEOUT_MS);
    }

    /**
     * Cancels any outstanding timer tasks.
     */
    private void cancelTimer() {
        ThreadUtils.checkIsOnMainThread();
        Log.d(TAG, "cancelTimer");
        handler.removeCallbacks(bluetoothTimeoutRunnable);
    }

    /**
     * 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() {
        ThreadUtils.checkIsOnMainThread();
        if (bluetoothState == State.UNINITIALIZED || bluetoothHeadset == null) {
            return;
        }
        Log.d(TAG, "bluetoothTimeout: BT state=" + bluetoothState + ", " + "attempts: " + scoConnectionAttempts + ", " + "SCO is on: " + isSreplaced());
        if (bluetoothState != State.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, "SCO connected with " + bluetoothDevice.getName());
                scoConnected = true;
            } else {
                Log.d(TAG, "SCO is not connected with " + bluetoothDevice.getName());
            }
        }
        if (scoConnected) {
            // We thought BT had timed out, but it's actually on; updating state.
            bluetoothState = State.SCO_CONNECTED;
            scoConnectionAttempts = 0;
        } else {
            // Give up and "cancel" our request by calling stopBluetoothSco().
            Log.w(TAG, "BT failed to connect after timeout");
            stopScoAudio();
        }
        updateAudioDeviceState();
        Log.d(TAG, "bluetoothTimeout done: BT state=" + bluetoothState);
    }

    /**
     * Checks whether audio uses Bluetooth SCO.
     */
    private boolean isSreplaced() {
        return audioManager.isBluetoothSreplaced();
    }

    /**
     * Converts BluetoothAdapter states into local string representations.
     */
    private String stateToString(int state) {
        switch(state) {
            case BluetoothAdapter.STATE_DISCONNECTED:
                return "DISCONNECTED";
            case BluetoothAdapter.STATE_CONNECTED:
                return "CONNECTED";
            case BluetoothAdapter.STATE_CONNECTING:
                return "CONNECTING";
            case BluetoothAdapter.STATE_DISCONNECTING:
                return "DISCONNECTING";
            case BluetoothAdapter.STATE_OFF:
                return "OFF";
            case BluetoothAdapter.STATE_ON:
                return "ON";
            case BluetoothAdapter.STATE_TURNING_OFF:
                // 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
                // for STATE_ON before attempting to use the adapter.
                return "TURNING_ON";
            default:
                return "INVALID";
        }
    }

    // Bluetooth connection state.
    public enum State {

        // Bluetooth is not available; no adapter or Bluetooth is off.
        UNINITIALIZED,
        // Bluetooth error happened when trying to start Bluetooth.
        ERROR,
        // Bluetooth proxy object for the Headset profile exists, but no connected headset devices,
        // SCO is not started or disconnected.
        HEADSET_UNAVAILABLE,
        // Bluetooth proxy object for the Headset profile 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
    }

    /**
     * Implementation of an interface that notifies BluetoothProfile IPC clients when they have been
     * connected to or disconnected from the service.
     */
    private clreplaced BluetoothServiceListener implements BluetoothProfile.ServiceListener {

        @Override
        public // connection and perform other operations that are relevant to the headset profile.
        void onServiceConnected(int profile, BluetoothProfile proxy) {
            if (profile != BluetoothProfile.HEADSET || bluetoothState == State.UNINITIALIZED) {
                return;
            }
            Log.d(TAG, "BluetoothServiceListener.onServiceConnected: BT state=" + bluetoothState);
            // Android only supports one connected Bluetooth Headset at a time.
            bluetoothHeadset = (BluetoothHeadset) proxy;
            updateAudioDeviceState();
            Log.d(TAG, "onServiceConnected done: BT state=" + bluetoothState);
        }

        @Override
        public /**
         * Notifies the client when the proxy object has been disconnected from the service.
         */
        void onServiceDisconnected(int profile) {
            if (profile != BluetoothProfile.HEADSET || bluetoothState == State.UNINITIALIZED) {
                return;
            }
            Log.d(TAG, "BluetoothServiceListener.onServiceDisconnected: BT state=" + bluetoothState);
            stopScoAudio();
            bluetoothHeadset = null;
            bluetoothDevice = null;
            bluetoothState = State.HEADSET_UNAVAILABLE;
            updateAudioDeviceState();
            Log.d(TAG, "onServiceDisconnected done: BT state=" + bluetoothState);
        }
    }

    // Intent broadcast receiver which handles changes in Bluetooth device availability.
    // Detects headset changes and Bluetooth SCO state changes.
    private clreplaced BluetoothHeadsetBroadcastReceiver extends BroadcastReceiver {

        @Override
        public void onReceive(Context context, Intent intent) {
            if (bluetoothState == State.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. Typically received when user turns on a BT
            // 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(TAG, "BluetoothHeadsetBroadcastReceiver.onReceive: " + "a=ACTION_CONNECTION_STATE_CHANGED, " + "s=" + stateToString(state) + ", " + "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.
                    stopScoAudio();
                    updateAudioDeviceState();
                }
            // 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(TAG, "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) {
                        Log.d(TAG, "+++ Bluetooth audio SCO is now connected");
                        bluetoothState = State.SCO_CONNECTED;
                        scoConnectionAttempts = 0;
                        updateAudioDeviceState();
                    } else {
                        Log.w(TAG, "Unexpected state BluetoothHeadset.STATE_AUDIO_CONNECTED");
                    }
                } else if (state == BluetoothHeadset.STATE_AUDIO_CONNECTING) {
                    Log.d(TAG, "+++ Bluetooth audio SCO is now connecting...");
                } else if (state == BluetoothHeadset.STATE_AUDIO_DISCONNECTED) {
                    Log.d(TAG, "+++ Bluetooth audio SCO is now disconnected");
                    if (isInitialStickyBroadcast()) {
                        Log.d(TAG, "Ignore STATE_AUDIO_DISCONNECTED initial sticky broadcast.");
                        return;
                    }
                    updateAudioDeviceState();
                }
            }
            Log.d(TAG, "onReceive done: BT state=" + bluetoothState);
        }
    }
}

17 Source : MagicBluetoothManager.java
with GNU General Public License v3.0
from nextcloud

public clreplaced MagicBluetoothManager {

    private static final String TAG = "MagicBluetoothManager";

    // 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;

    private final Context apprtcContext;

    private final MagicAudioManager apprtcAudioManager;

    private final AudioManager audioManager;

    private final Handler handler;

    private final BluetoothProfile.ServiceListener bluetoothServiceListener;

    private final BroadcastReceiver bluetoothHeadsetReceiver;

    int scoConnectionAttempts;

    private State bluetoothState;

    private BluetoothAdapter bluetoothAdapter;

    private BluetoothHeadset bluetoothHeadset;

    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();
        }
    };

    protected MagicBluetoothManager(Context context, MagicAudioManager audioManager) {
        Log.d(TAG, "ctor");
        ThreadUtils.checkIsOnMainThread();
        apprtcContext = context;
        apprtcAudioManager = audioManager;
        this.audioManager = getAudioManager(context);
        bluetoothState = State.UNINITIALIZED;
        bluetoothServiceListener = new BluetoothServiceListener();
        bluetoothHeadsetReceiver = new BluetoothHeadsetBroadcastReceiver();
        handler = new Handler(Looper.getMainLooper());
    }

    /**
     * Construction.
     */
    static MagicBluetoothManager create(Context context, MagicAudioManager audioManager) {
        return new MagicBluetoothManager(context, audioManager);
    }

    /**
     * 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 MagicAudioManager is also involved in driving this state
     * change.
     */
    public void start() {
        ThreadUtils.checkIsOnMainThread();
        Log.d(TAG, "start");
        if (!hasPermission(apprtcContext, android.Manifest.permission.BLUETOOTH)) {
            Log.w(TAG, "Process (pid=" + Process.myPid() + ") lacks BLUETOOTH permission");
            return;
        }
        if (bluetoothState != State.UNINITIALIZED) {
            Log.w(TAG, "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, "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, "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(apprtcContext, bluetoothServiceListener, BluetoothProfile.HEADSET)) {
            Log.e(TAG, "BluetoothAdapter.getProfileProxy(HEADSET) failed");
            return;
        }
        // Register receivers for BluetoothHeadset change notifications.
        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, "HEADSET profile state: " + stateToString(bluetoothAdapter.getProfileConnectionState(BluetoothProfile.HEADSET)));
        Log.d(TAG, "Bluetooth proxy for headset profile has started");
        bluetoothState = State.HEADSET_UNAVAILABLE;
        Log.d(TAG, "start done: BT state=" + bluetoothState);
    }

    /**
     * Stops and closes all components related to Bluetooth audio.
     */
    public void stop() {
        ThreadUtils.checkIsOnMainThread();
        Log.d(TAG, "stop: BT state=" + bluetoothState);
        if (bluetoothAdapter == null) {
            return;
        }
        // Stop BT SCO connection with remote device if needed.
        stopScoAudio();
        // Close down remaining BT resources.
        if (bluetoothState == State.UNINITIALIZED) {
            return;
        }
        unregisterReceiver(bluetoothHeadsetReceiver);
        cancelTimer();
        if (bluetoothHeadset != null) {
            bluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, bluetoothHeadset);
            bluetoothHeadset = null;
        }
        bluetoothAdapter = null;
        bluetoothDevice = null;
        bluetoothState = State.UNINITIALIZED;
        Log.d(TAG, "stop done: BT state=" + bluetoothState);
    }

    /**
     * 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(TAG, "startSco: BT state=" + bluetoothState + ", " + "attempts: " + scoConnectionAttempts + ", " + "SCO is on: " + isSreplaced());
        if (scoConnectionAttempts >= MAX_SCO_CONNECTION_ATTEMPTS) {
            Log.e(TAG, "BT SCO connection fails - no more attempts");
            return false;
        }
        if (bluetoothState != State.HEADSET_AVAILABLE) {
            Log.e(TAG, "BT SCO connection fails - no headset available");
            return false;
        }
        // Start BT SCO channel and wait for ACTION_AUDIO_STATE_CHANGED.
        Log.d(TAG, "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.
        bluetoothState = State.SCO_CONNECTING;
        audioManager.startBluetoothSco();
        audioManager.setBluetoothSreplaced(true);
        scoConnectionAttempts++;
        startTimer();
        Log.d(TAG, "startScoAudio done: BT state=" + bluetoothState + ", " + "SCO is on: " + isSreplaced());
        return true;
    }

    /**
     * Stops Bluetooth SCO connection with remote device.
     */
    public void stopScoAudio() {
        ThreadUtils.checkIsOnMainThread();
        Log.d(TAG, "stopScoAudio: BT state=" + bluetoothState + ", " + "SCO is on: " + isSreplaced());
        if (bluetoothState != State.SCO_CONNECTING && bluetoothState != State.SCO_CONNECTED) {
            return;
        }
        cancelTimer();
        audioManager.stopBluetoothSco();
        audioManager.setBluetoothSreplaced(false);
        bluetoothState = State.SCO_DISCONNECTING;
        Log.d(TAG, "stopScoAudio done: BT state=" + bluetoothState + ", " + "SCO is on: " + isSreplaced());
    }

    /**
     * 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.
     */
    public void updateDevice() {
        if (bluetoothState == State.UNINITIALIZED || bluetoothHeadset == null) {
            return;
        }
        Log.d(TAG, "updateDevice");
        // Get connected devices for the headset profile. Returns the set of
        // devices which are in state STATE_CONNECTED. The BluetoothDevice clreplaced
        // is just a thin wrapper for a Bluetooth hardware address.
        List<BluetoothDevice> devices = bluetoothHeadset.getConnectedDevices();
        if (devices.isEmpty()) {
            bluetoothDevice = null;
            bluetoothState = State.HEADSET_UNAVAILABLE;
            Log.d(TAG, "No connected bluetooth headset");
        } else {
            // Always use first device in list. Android only supports one device.
            bluetoothDevice = devices.get(0);
            bluetoothState = State.HEADSET_AVAILABLE;
            Log.d(TAG, "Connected bluetooth headset: " + "name=" + bluetoothDevice.getName() + ", " + "state=" + stateToString(bluetoothHeadset.getConnectionState(bluetoothDevice)) + ", SCO audio=" + bluetoothHeadset.isAudioConnected(bluetoothDevice));
        }
        Log.d(TAG, "updateDevice done: BT state=" + bluetoothState);
    }

    /**
     * Stubs for test mocks.
     */
    protected AudioManager getAudioManager(Context context) {
        return (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
    }

    protected void registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
        apprtcContext.registerReceiver(receiver, filter);
    }

    protected void unregisterReceiver(BroadcastReceiver receiver) {
        apprtcContext.unregisterReceiver(receiver);
    }

    protected boolean getBluetoothProfileProxy(Context context, BluetoothProfile.ServiceListener listener, int profile) {
        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(TAG, "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(TAG, "paired devices:");
            for (BluetoothDevice device : pairedDevices) {
                Log.d(TAG, " name=" + device.getName() + ", address=" + device.getAddress());
            }
        }
    }

    /**
     * Ensures that the audio manager updates its list of available audio devices.
     */
    private void updateAudioDeviceState() {
        ThreadUtils.checkIsOnMainThread();
        Log.d(TAG, "updateAudioDeviceState");
        apprtcAudioManager.updateAudioDeviceState();
    }

    /**
     * Starts timer which times out after BLUETOOTH_SCO_TIMEOUT_MS milliseconds.
     */
    private void startTimer() {
        ThreadUtils.checkIsOnMainThread();
        Log.d(TAG, "startTimer");
        handler.postDelayed(bluetoothTimeoutRunnable, BLUETOOTH_SCO_TIMEOUT_MS);
    }

    /**
     * Cancels any outstanding timer tasks.
     */
    private void cancelTimer() {
        ThreadUtils.checkIsOnMainThread();
        Log.d(TAG, "cancelTimer");
        handler.removeCallbacks(bluetoothTimeoutRunnable);
    }

    /**
     * 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() {
        ThreadUtils.checkIsOnMainThread();
        if (bluetoothState == State.UNINITIALIZED || bluetoothHeadset == null) {
            return;
        }
        Log.d(TAG, "bluetoothTimeout: BT state=" + bluetoothState + ", " + "attempts: " + scoConnectionAttempts + ", " + "SCO is on: " + isSreplaced());
        if (bluetoothState != State.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, "SCO connected with " + bluetoothDevice.getName());
                scoConnected = true;
            } else {
                Log.d(TAG, "SCO is not connected with " + bluetoothDevice.getName());
            }
        }
        if (scoConnected) {
            // We thought BT had timed out, but it's actually on; updating state.
            bluetoothState = State.SCO_CONNECTED;
            scoConnectionAttempts = 0;
        } else {
            // Give up and "cancel" our request by calling stopBluetoothSco().
            Log.w(TAG, "BT failed to connect after timeout");
            stopScoAudio();
        }
        updateAudioDeviceState();
        Log.d(TAG, "bluetoothTimeout done: BT state=" + bluetoothState);
    }

    /**
     * Checks whether audio uses Bluetooth SCO.
     */
    private boolean isSreplaced() {
        return audioManager.isBluetoothSreplaced();
    }

    /**
     * Converts BluetoothAdapter states into local string representations.
     */
    private String stateToString(int state) {
        switch(state) {
            case BluetoothAdapter.STATE_DISCONNECTED:
                return "DISCONNECTED";
            case BluetoothAdapter.STATE_CONNECTED:
                return "CONNECTED";
            case BluetoothAdapter.STATE_CONNECTING:
                return "CONNECTING";
            case BluetoothAdapter.STATE_DISCONNECTING:
                return "DISCONNECTING";
            case BluetoothAdapter.STATE_OFF:
                return "OFF";
            case BluetoothAdapter.STATE_ON:
                return "ON";
            case BluetoothAdapter.STATE_TURNING_OFF:
                // 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
                // for STATE_ON before attempting to use the adapter.
                return "TURNING_ON";
            default:
                return "INVALID";
        }
    }

    // Bluetooth connection state.
    public enum State {

        // Bluetooth is not available; no adapter or Bluetooth is off.
        UNINITIALIZED,
        // Bluetooth error happened when trying to start Bluetooth.
        ERROR,
        // Bluetooth proxy object for the Headset profile exists, but no connected headset devices,
        // SCO is not started or disconnected.
        HEADSET_UNAVAILABLE,
        // Bluetooth proxy object for the Headset profile 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
    }

    /**
     * Implementation of an interface that notifies BluetoothProfile IPC clients when they have been
     * connected to or disconnected from the service.
     */
    private clreplaced BluetoothServiceListener implements BluetoothProfile.ServiceListener {

        @Override
        public // connection and perform other operations that are relevant to the headset profile.
        void onServiceConnected(int profile, BluetoothProfile proxy) {
            if (profile != BluetoothProfile.HEADSET || bluetoothState == State.UNINITIALIZED) {
                return;
            }
            Log.d(TAG, "BluetoothServiceListener.onServiceConnected: BT state=" + bluetoothState);
            // Android only supports one connected Bluetooth Headset at a time.
            bluetoothHeadset = (BluetoothHeadset) proxy;
            updateAudioDeviceState();
            Log.d(TAG, "onServiceConnected done: BT state=" + bluetoothState);
        }

        @Override
        public /**
         * Notifies the client when the proxy object has been disconnected from the service.
         */
        void onServiceDisconnected(int profile) {
            if (profile != BluetoothProfile.HEADSET || bluetoothState == State.UNINITIALIZED) {
                return;
            }
            Log.d(TAG, "BluetoothServiceListener.onServiceDisconnected: BT state=" + bluetoothState);
            stopScoAudio();
            bluetoothHeadset = null;
            bluetoothDevice = null;
            bluetoothState = State.HEADSET_UNAVAILABLE;
            updateAudioDeviceState();
            Log.d(TAG, "onServiceDisconnected done: BT state=" + bluetoothState);
        }
    }

    // Intent broadcast receiver which handles changes in Bluetooth device availability.
    // Detects headset changes and Bluetooth SCO state changes.
    private clreplaced BluetoothHeadsetBroadcastReceiver extends BroadcastReceiver {

        @Override
        public void onReceive(Context context, Intent intent) {
            if (bluetoothState == State.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. Typically received when user turns on a BT
            // 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(TAG, "BluetoothHeadsetBroadcastReceiver.onReceive: " + "a=ACTION_CONNECTION_STATE_CHANGED, " + "s=" + stateToString(state) + ", " + "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.
                    stopScoAudio();
                    updateAudioDeviceState();
                }
            // 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(TAG, "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) {
                        Log.d(TAG, "+++ Bluetooth audio SCO is now connected");
                        bluetoothState = State.SCO_CONNECTED;
                        scoConnectionAttempts = 0;
                        updateAudioDeviceState();
                    } else {
                        Log.w(TAG, "Unexpected state BluetoothHeadset.STATE_AUDIO_CONNECTED");
                    }
                } else if (state == BluetoothHeadset.STATE_AUDIO_CONNECTING) {
                    Log.d(TAG, "+++ Bluetooth audio SCO is now connecting...");
                } else if (state == BluetoothHeadset.STATE_AUDIO_DISCONNECTED) {
                    Log.d(TAG, "+++ Bluetooth audio SCO is now disconnected");
                    if (isInitialStickyBroadcast()) {
                        Log.d(TAG, "Ignore STATE_AUDIO_DISCONNECTED initial sticky broadcast.");
                        return;
                    }
                    updateAudioDeviceState();
                }
            }
            Log.d(TAG, "onReceive done: BT state=" + bluetoothState);
        }
    }
}

17 Source : AppRTCBluetoothManager.java
with MIT License
from lgyjg

/**
 * AppRTCProximitySensor manages functions related to Bluetoth devices in the
 * AppRTC demo.
 */
public clreplaced AppRTCBluetoothManager {

    private static final String TAG = "AppRTCBluetoothManager";

    // 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;

    private final Context apprtcContext;

    private final AppRTCAudioManager apprtcAudioManager;

    private final AudioManager audioManager;

    private final Handler handler;

    private final BluetoothProfile.ServiceListener bluetoothServiceListener;

    private final BroadcastReceiver bluetoothHeadsetReceiver;

    int scoConnectionAttempts;

    private State bluetoothState;

    private BluetoothAdapter bluetoothAdapter;

    private BluetoothHeadset bluetoothHeadset;

    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();
        }
    };

    protected AppRTCBluetoothManager(Context context, AppRTCAudioManager audioManager) {
        Log.d(TAG, "ctor");
        ThreadUtils.checkIsOnMainThread();
        apprtcContext = context;
        apprtcAudioManager = audioManager;
        this.audioManager = getAudioManager(context);
        bluetoothState = State.UNINITIALIZED;
        bluetoothServiceListener = new BluetoothServiceListener();
        bluetoothHeadsetReceiver = new BluetoothHeadsetBroadcastReceiver();
        handler = new Handler(Looper.getMainLooper());
    }

    /**
     * Construction.
     */
    static AppRTCBluetoothManager create(Context context, AppRTCAudioManager audioManager) {
        Log.d(TAG, "create" + AppRTCUtils.getThreadInfo());
        return new AppRTCBluetoothManager(context, audioManager);
    }

    /**
     * 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.
     */
    public void start() {
        ThreadUtils.checkIsOnMainThread();
        Log.d(TAG, "start");
        if (!hasPermission(apprtcContext, android.Manifest.permission.BLUETOOTH)) {
            Log.w(TAG, "Process (pid=" + Process.myPid() + ") lacks BLUETOOTH permission");
            return;
        }
        if (bluetoothState != State.UNINITIALIZED) {
            Log.w(TAG, "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, "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, "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(apprtcContext, bluetoothServiceListener, BluetoothProfile.HEADSET)) {
            Log.e(TAG, "BluetoothAdapter.getProfileProxy(HEADSET) failed");
            return;
        }
        // Register receivers for BluetoothHeadset change notifications.
        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, "HEADSET profile state: " + stateToString(bluetoothAdapter.getProfileConnectionState(BluetoothProfile.HEADSET)));
        Log.d(TAG, "Bluetooth proxy for headset profile has started");
        bluetoothState = State.HEADSET_UNAVAILABLE;
        Log.d(TAG, "start done: BT state=" + bluetoothState);
    }

    /**
     * Stops and closes all components related to Bluetooth audio.
     */
    public void stop() {
        ThreadUtils.checkIsOnMainThread();
        unregisterReceiver(bluetoothHeadsetReceiver);
        Log.d(TAG, "stop: BT state=" + bluetoothState);
        if (bluetoothAdapter != null) {
            // Stop BT SCO connection with remote device if needed.
            stopScoAudio();
            // Close down remaining BT resources.
            if (bluetoothState != State.UNINITIALIZED) {
                cancelTimer();
                if (bluetoothHeadset != null) {
                    bluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, bluetoothHeadset);
                    bluetoothHeadset = null;
                }
                bluetoothAdapter = null;
                bluetoothDevice = null;
                bluetoothState = State.UNINITIALIZED;
            }
        }
        Log.d(TAG, "stop done: BT state=" + bluetoothState);
    }

    /**
     * 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(TAG, "startSco: BT state=" + bluetoothState + ", " + "attempts: " + scoConnectionAttempts + ", " + "SCO is on: " + isSreplaced());
        if (scoConnectionAttempts >= MAX_SCO_CONNECTION_ATTEMPTS) {
            Log.e(TAG, "BT SCO connection fails - no more attempts");
            return false;
        }
        if (bluetoothState != State.HEADSET_AVAILABLE) {
            Log.e(TAG, "BT SCO connection fails - no headset available");
            return false;
        }
        // Start BT SCO channel and wait for ACTION_AUDIO_STATE_CHANGED.
        Log.d(TAG, "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.
        bluetoothState = State.SCO_CONNECTING;
        audioManager.startBluetoothSco();
        scoConnectionAttempts++;
        startTimer();
        Log.d(TAG, "startScoAudio done: BT state=" + bluetoothState);
        return true;
    }

    /**
     * Stops Bluetooth SCO connection with remote device.
     */
    public void stopScoAudio() {
        ThreadUtils.checkIsOnMainThread();
        Log.d(TAG, "stopScoAudio: BT state=" + bluetoothState + ", " + "SCO is on: " + isSreplaced());
        if (bluetoothState != State.SCO_CONNECTING && bluetoothState != State.SCO_CONNECTED) {
            return;
        }
        cancelTimer();
        audioManager.stopBluetoothSco();
        bluetoothState = State.SCO_DISCONNECTING;
        Log.d(TAG, "stopScoAudio done: BT state=" + bluetoothState);
    }

    /**
     * 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.
     */
    public void updateDevice() {
        if (bluetoothState == State.UNINITIALIZED || bluetoothHeadset == null) {
            return;
        }
        Log.d(TAG, "updateDevice");
        // Get connected devices for the headset profile. Returns the set of
        // devices which are in state STATE_CONNECTED. The BluetoothDevice clreplaced
        // is just a thin wrapper for a Bluetooth hardware address.
        List<BluetoothDevice> devices = bluetoothHeadset.getConnectedDevices();
        if (devices.isEmpty()) {
            bluetoothDevice = null;
            bluetoothState = State.HEADSET_UNAVAILABLE;
            Log.d(TAG, "No connected bluetooth headset");
        } else {
            // Always use first device is list. Android only supports one device.
            bluetoothDevice = devices.get(0);
            bluetoothState = State.HEADSET_AVAILABLE;
            Log.d(TAG, "Connected bluetooth headset: " + "name=" + bluetoothDevice.getName() + ", " + "state=" + stateToString(bluetoothHeadset.getConnectionState(bluetoothDevice)) + ", SCO audio=" + bluetoothHeadset.isAudioConnected(bluetoothDevice));
        }
        Log.d(TAG, "updateDevice done: BT state=" + bluetoothState);
    }

    /**
     * Stubs for test mocks.
     */
    protected AudioManager getAudioManager(Context context) {
        return (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
    }

    protected void registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
        apprtcContext.registerReceiver(receiver, filter);
    }

    protected void unregisterReceiver(BroadcastReceiver receiver) {
        apprtcContext.unregisterReceiver(receiver);
    }

    protected boolean getBluetoothProfileProxy(Context context, BluetoothProfile.ServiceListener listener, int profile) {
        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.
     */
    protected void logBluetoothAdapterInfo(BluetoothAdapter localAdapter) {
        Log.d(TAG, "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(TAG, "paired devices:");
            for (BluetoothDevice device : pairedDevices) {
                Log.d(TAG, " name=" + device.getName() + ", address=" + device.getAddress());
            }
        }
    }

    /**
     * Ensures that the audio manager updates its list of available audio devices.
     */
    private void updateAudioDeviceState() {
        ThreadUtils.checkIsOnMainThread();
        Log.d(TAG, "updateAudioDeviceState");
        apprtcAudioManager.updateAudioDeviceState();
    }

    /**
     * Starts timer which times out after BLUETOOTH_SCO_TIMEOUT_MS milliseconds.
     */
    private void startTimer() {
        ThreadUtils.checkIsOnMainThread();
        Log.d(TAG, "startTimer");
        handler.postDelayed(bluetoothTimeoutRunnable, BLUETOOTH_SCO_TIMEOUT_MS);
    }

    /**
     * Cancels any outstanding timer tasks.
     */
    private void cancelTimer() {
        ThreadUtils.checkIsOnMainThread();
        Log.d(TAG, "cancelTimer");
        handler.removeCallbacks(bluetoothTimeoutRunnable);
    }

    /**
     * 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() {
        ThreadUtils.checkIsOnMainThread();
        if (bluetoothState == State.UNINITIALIZED || bluetoothHeadset == null) {
            return;
        }
        Log.d(TAG, "bluetoothTimeout: BT state=" + bluetoothState + ", " + "attempts: " + scoConnectionAttempts + ", " + "SCO is on: " + isSreplaced());
        if (bluetoothState != State.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, "SCO connected with " + bluetoothDevice.getName());
                scoConnected = true;
            } else {
                Log.d(TAG, "SCO is not connected with " + bluetoothDevice.getName());
            }
        }
        if (scoConnected) {
            // We thought BT had timed out, but it's actually on; updating state.
            bluetoothState = State.SCO_CONNECTED;
            scoConnectionAttempts = 0;
        } else {
            // Give up and "cancel" our request by calling stopBluetoothSco().
            Log.w(TAG, "BT failed to connect after timeout");
            stopScoAudio();
        }
        updateAudioDeviceState();
        Log.d(TAG, "bluetoothTimeout done: BT state=" + bluetoothState);
    }

    /**
     * Checks whether audio uses Bluetooth SCO.
     */
    private boolean isSreplaced() {
        return audioManager.isBluetoothSreplaced();
    }

    /**
     * Converts BluetoothAdapter states into local string representations.
     */
    private String stateToString(int state) {
        switch(state) {
            case BluetoothAdapter.STATE_DISCONNECTED:
                return "DISCONNECTED";
            case BluetoothAdapter.STATE_CONNECTED:
                return "CONNECTED";
            case BluetoothAdapter.STATE_CONNECTING:
                return "CONNECTING";
            case BluetoothAdapter.STATE_DISCONNECTING:
                return "DISCONNECTING";
            case BluetoothAdapter.STATE_OFF:
                return "OFF";
            case BluetoothAdapter.STATE_ON:
                return "ON";
            case BluetoothAdapter.STATE_TURNING_OFF:
                // 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
                // for STATE_ON before attempting to use the adapter.
                return "TURNING_ON";
            default:
                return "INVALID";
        }
    }

    // Bluetooth connection state.
    public enum State {

        // Bluetooth is not available; no adapter or Bluetooth is off.
        UNINITIALIZED,
        // Bluetooth error happened when trying to start Bluetooth.
        ERROR,
        // Bluetooth proxy object for the Headset profile exists, but no connected headset devices,
        // SCO is not started or disconnected.
        HEADSET_UNAVAILABLE,
        // Bluetooth proxy object for the Headset profile 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
    }

    /**
     * Implementation of an interface that notifies BluetoothProfile IPC clients when they have been
     * connected to or disconnected from the service.
     */
    private clreplaced BluetoothServiceListener implements BluetoothProfile.ServiceListener {

        @Override
        public // connection and perform other operations that are relevant to the headset profile.
        void onServiceConnected(int profile, BluetoothProfile proxy) {
            if (profile != BluetoothProfile.HEADSET || bluetoothState == State.UNINITIALIZED) {
                return;
            }
            Log.d(TAG, "BluetoothServiceListener.onServiceConnected: BT state=" + bluetoothState);
            // Android only supports one connected Bluetooth Headset at a time.
            bluetoothHeadset = (BluetoothHeadset) proxy;
            updateAudioDeviceState();
            Log.d(TAG, "onServiceConnected done: BT state=" + bluetoothState);
        }

        @Override
        public /**
         * Notifies the client when the proxy object has been disconnected from the service.
         */
        void onServiceDisconnected(int profile) {
            if (profile != BluetoothProfile.HEADSET || bluetoothState == State.UNINITIALIZED) {
                return;
            }
            Log.d(TAG, "BluetoothServiceListener.onServiceDisconnected: BT state=" + bluetoothState);
            stopScoAudio();
            bluetoothHeadset = null;
            bluetoothDevice = null;
            bluetoothState = State.HEADSET_UNAVAILABLE;
            updateAudioDeviceState();
            Log.d(TAG, "onServiceDisconnected done: BT state=" + bluetoothState);
        }
    }

    // Intent broadcast receiver which handles changes in Bluetooth device availability.
    // Detects headset changes and Bluetooth SCO state changes.
    private clreplaced BluetoothHeadsetBroadcastReceiver extends BroadcastReceiver {

        @Override
        public void onReceive(Context context, Intent intent) {
            if (bluetoothState == State.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. Typically received when user turns on a BT
            // 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(TAG, "BluetoothHeadsetBroadcastReceiver.onReceive: " + "a=ACTION_CONNECTION_STATE_CHANGED, " + "s=" + stateToString(state) + ", " + "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.
                    stopScoAudio();
                    updateAudioDeviceState();
                }
            // 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(TAG, "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) {
                        Log.d(TAG, "+++ Bluetooth audio SCO is now connected");
                        bluetoothState = State.SCO_CONNECTED;
                        scoConnectionAttempts = 0;
                        updateAudioDeviceState();
                    } else {
                        Log.w(TAG, "Unexpected state BluetoothHeadset.STATE_AUDIO_CONNECTED");
                    }
                } else if (state == BluetoothHeadset.STATE_AUDIO_CONNECTING) {
                    Log.d(TAG, "+++ Bluetooth audio SCO is now connecting...");
                } else if (state == BluetoothHeadset.STATE_AUDIO_DISCONNECTED) {
                    Log.d(TAG, "+++ Bluetooth audio SCO is now disconnected");
                    if (isInitialStickyBroadcast()) {
                        Log.d(TAG, "Ignore STATE_AUDIO_DISCONNECTED initial sticky broadcast.");
                        return;
                    }
                    updateAudioDeviceState();
                }
            }
            Log.d(TAG, "onReceive done: BT state=" + bluetoothState);
        }
    }
}

17 Source : CallBluetoothManager.java
with GNU Affero General Public License v3.0
from KianIranian-STDG

/**
 * AppRTCProximitySensor manages functions related to Bluetoth devices in the
 * AppRTC demo.
 */
public clreplaced CallBluetoothManager {

    private static final String TAG = "AppRTCBluetoothManager";

    // 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;

    // Bluetooth connection state.
    public enum State {

        // Bluetooth is not available; no adapter or Bluetooth is off.
        UNINITIALIZED,
        // Bluetooth error happened when trying to start Bluetooth.
        ERROR,
        // Bluetooth proxy object for the Headset profile exists, but no connected headset devices,
        // SCO is not started or disconnected.
        HEADSET_UNAVAILABLE,
        // Bluetooth proxy object for the Headset profile 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
    }

    private final Context apprtcContext;

    private final CallAudioManager apprtcAudioManager;

    private final AudioManager audioManager;

    private final Handler handler;

    int scoConnectionAttempts;

    private State bluetoothState;

    private final BluetoothProfile.ServiceListener bluetoothServiceListener;

    private BluetoothAdapter bluetoothAdapter;

    private BluetoothHeadset bluetoothHeadset;

    private BluetoothDevice bluetoothDevice;

    private final BroadcastReceiver bluetoothHeadsetReceiver;

    // 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();
        }
    };

    /**
     * Implementation of an interface that notifies BluetoothProfile IPC clients when they have been
     * connected to or disconnected from the service.
     */
    private clreplaced BluetoothServiceListener implements BluetoothProfile.ServiceListener {

        @Override
        public // connection and perform other operations that are relevant to the headset profile.
        void onServiceConnected(int profile, BluetoothProfile proxy) {
            if (profile != BluetoothProfile.HEADSET || bluetoothState == State.UNINITIALIZED) {
                return;
            }
            Log.d(TAG, "BluetoothServiceListener.onServiceConnected: BT state=" + bluetoothState);
            // Android only supports one connected Bluetooth Headset at a time.
            bluetoothHeadset = (BluetoothHeadset) proxy;
            updateAudioDeviceState();
            Log.d(TAG, "onServiceConnected done: BT state=" + bluetoothState);
        }

        @Override
        public /**
         * Notifies the client when the proxy object has been disconnected from the service.
         */
        void onServiceDisconnected(int profile) {
            if (profile != BluetoothProfile.HEADSET || bluetoothState == State.UNINITIALIZED) {
                return;
            }
            Log.d(TAG, "BluetoothServiceListener.onServiceDisconnected: BT state=" + bluetoothState);
            stopScoAudio();
            bluetoothHeadset = null;
            bluetoothDevice = null;
            bluetoothState = State.HEADSET_UNAVAILABLE;
            updateAudioDeviceState();
            Log.d(TAG, "onServiceDisconnected done: BT state=" + bluetoothState);
        }
    }

    // Intent broadcast receiver which handles changes in Bluetooth device availability.
    // Detects headset changes and Bluetooth SCO state changes.
    private clreplaced BluetoothHeadsetBroadcastReceiver extends BroadcastReceiver {

        @Override
        public void onReceive(Context context, Intent intent) {
            if (bluetoothState == State.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. Typically received when user turns on a BT
            // 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(TAG, "BluetoothHeadsetBroadcastReceiver.onReceive: " + "a=ACTION_CONNECTION_STATE_CHANGED, " + "s=" + stateToString(state) + ", " + "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.
                    stopScoAudio();
                    updateAudioDeviceState();
                }
            // 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(TAG, "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) {
                        Log.d(TAG, "+++ Bluetooth audio SCO is now connected");
                        bluetoothState = State.SCO_CONNECTED;
                        scoConnectionAttempts = 0;
                        updateAudioDeviceState();
                    } else {
                        Log.w(TAG, "Unexpected state BluetoothHeadset.STATE_AUDIO_CONNECTED");
                    }
                } else if (state == BluetoothHeadset.STATE_AUDIO_CONNECTING) {
                    Log.d(TAG, "+++ Bluetooth audio SCO is now connecting...");
                } else if (state == BluetoothHeadset.STATE_AUDIO_DISCONNECTED) {
                    Log.d(TAG, "+++ Bluetooth audio SCO is now disconnected");
                    if (isInitialStickyBroadcast()) {
                        Log.d(TAG, "Ignore STATE_AUDIO_DISCONNECTED initial sticky broadcast.");
                        return;
                    }
                    updateAudioDeviceState();
                }
            }
            Log.d(TAG, "onReceive done: BT state=" + bluetoothState);
        }
    }

    /**
     * Construction.
     */
    static CallBluetoothManager create(Context context, CallAudioManager audioManager) {
        Log.d(TAG, "create" + AppRTCUtils.getThreadInfo());
        return new CallBluetoothManager(context, audioManager);
    }

    protected CallBluetoothManager(Context context, CallAudioManager audioManager) {
        Log.d(TAG, "ctor");
        ThreadUtils.checkIsOnMainThread();
        apprtcContext = context;
        apprtcAudioManager = audioManager;
        this.audioManager = getAudioManager(context);
        bluetoothState = State.UNINITIALIZED;
        bluetoothServiceListener = new BluetoothServiceListener();
        bluetoothHeadsetReceiver = new BluetoothHeadsetBroadcastReceiver();
        handler = new Handler(Looper.getMainLooper());
    }

    /**
     * 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.
     */
    public void start() {
        ThreadUtils.checkIsOnMainThread();
        Log.d(TAG, "start");
        if (!hasPermission(apprtcContext, android.Manifest.permission.BLUETOOTH)) {
            Log.w(TAG, "Process (pid=" + Process.myPid() + ") lacks BLUETOOTH permission");
            return;
        }
        if (bluetoothState != State.UNINITIALIZED) {
            Log.w(TAG, "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, "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, "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(apprtcContext, bluetoothServiceListener, BluetoothProfile.HEADSET)) {
            Log.e(TAG, "BluetoothAdapter.getProfileProxy(HEADSET) failed");
            return;
        }
        // Register receivers for BluetoothHeadset change notifications.
        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, "HEADSET profile state: " + stateToString(bluetoothAdapter.getProfileConnectionState(BluetoothProfile.HEADSET)));
        Log.d(TAG, "Bluetooth proxy for headset profile has started");
        bluetoothState = State.HEADSET_UNAVAILABLE;
        Log.d(TAG, "start done: BT state=" + bluetoothState);
    }

    /**
     * Stops and closes all components related to Bluetooth audio.
     */
    public void stop() {
        ThreadUtils.checkIsOnMainThread();
        Log.d(TAG, "stop: BT state=" + bluetoothState);
        if (bluetoothAdapter == null) {
            return;
        }
        // Stop BT SCO connection with remote device if needed.
        stopScoAudio();
        // Close down remaining BT resources.
        if (bluetoothState == State.UNINITIALIZED) {
            return;
        }
        unregisterReceiver(bluetoothHeadsetReceiver);
        cancelTimer();
        if (bluetoothHeadset != null) {
            bluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, bluetoothHeadset);
            bluetoothHeadset = null;
        }
        bluetoothAdapter = null;
        bluetoothDevice = null;
        bluetoothState = State.UNINITIALIZED;
        Log.d(TAG, "stop done: BT state=" + bluetoothState);
    }

    /**
     * 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(TAG, "startSco: BT state=" + bluetoothState + ", " + "attempts: " + scoConnectionAttempts + ", " + "SCO is on: " + isSreplaced());
        if (scoConnectionAttempts >= MAX_SCO_CONNECTION_ATTEMPTS) {
            Log.e(TAG, "BT SCO connection fails - no more attempts");
            return false;
        }
        if (bluetoothState != State.HEADSET_AVAILABLE) {
            Log.e(TAG, "BT SCO connection fails - no headset available");
            return false;
        }
        // Start BT SCO channel and wait for ACTION_AUDIO_STATE_CHANGED.
        Log.d(TAG, "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.
        bluetoothState = State.SCO_CONNECTING;
        audioManager.startBluetoothSco();
        audioManager.setBluetoothSreplaced(true);
        scoConnectionAttempts++;
        startTimer();
        Log.d(TAG, "startScoAudio done: BT state=" + bluetoothState + ", " + "SCO is on: " + isSreplaced());
        return true;
    }

    /**
     * Stops Bluetooth SCO connection with remote device.
     */
    public void stopScoAudio() {
        ThreadUtils.checkIsOnMainThread();
        Log.d(TAG, "stopScoAudio: BT state=" + bluetoothState + ", " + "SCO is on: " + isSreplaced());
        if (bluetoothState != State.SCO_CONNECTING && bluetoothState != State.SCO_CONNECTED) {
            return;
        }
        cancelTimer();
        audioManager.stopBluetoothSco();
        audioManager.setBluetoothSreplaced(false);
        bluetoothState = State.SCO_DISCONNECTING;
        Log.d(TAG, "stopScoAudio done: BT state=" + bluetoothState + ", " + "SCO is on: " + isSreplaced());
    }

    /**
     * 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.
     */
    public void updateDevice() {
        if (bluetoothState == State.UNINITIALIZED || bluetoothHeadset == null) {
            return;
        }
        Log.d(TAG, "updateDevice");
        // Get connected devices for the headset profile. Returns the set of
        // devices which are in state STATE_CONNECTED. The BluetoothDevice clreplaced
        // is just a thin wrapper for a Bluetooth hardware address.
        List<BluetoothDevice> devices = bluetoothHeadset.getConnectedDevices();
        if (devices.isEmpty()) {
            bluetoothDevice = null;
            bluetoothState = State.HEADSET_UNAVAILABLE;
            Log.d(TAG, "No connected bluetooth headset");
        } else {
            // Always use first device in list. Android only supports one device.
            bluetoothDevice = devices.get(0);
            bluetoothState = State.HEADSET_AVAILABLE;
            Log.d(TAG, "Connected bluetooth headset: " + "name=" + bluetoothDevice.getName() + ", " + "state=" + stateToString(bluetoothHeadset.getConnectionState(bluetoothDevice)) + ", SCO audio=" + bluetoothHeadset.isAudioConnected(bluetoothDevice));
        }
        Log.d(TAG, "updateDevice done: BT state=" + bluetoothState);
    }

    /**
     * Stubs for test mocks.
     */
    protected AudioManager getAudioManager(Context context) {
        return (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
    }

    protected void registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
        apprtcContext.registerReceiver(receiver, filter);
    }

    protected void unregisterReceiver(BroadcastReceiver receiver) {
        apprtcContext.unregisterReceiver(receiver);
    }

    protected boolean getBluetoothProfileProxy(Context context, BluetoothProfile.ServiceListener listener, int profile) {
        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(TAG, "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(TAG, "paired devices:");
            for (BluetoothDevice device : pairedDevices) {
                Log.d(TAG, " name=" + device.getName() + ", address=" + device.getAddress());
            }
        }
    }

    /**
     * Ensures that the audio manager updates its list of available audio devices.
     */
    private void updateAudioDeviceState() {
        ThreadUtils.checkIsOnMainThread();
        Log.d(TAG, "updateAudioDeviceState");
        apprtcAudioManager.updateAudioDeviceState();
    }

    /**
     * Starts timer which times out after BLUETOOTH_SCO_TIMEOUT_MS milliseconds.
     */
    private void startTimer() {
        ThreadUtils.checkIsOnMainThread();
        Log.d(TAG, "startTimer");
        handler.postDelayed(bluetoothTimeoutRunnable, BLUETOOTH_SCO_TIMEOUT_MS);
    }

    /**
     * Cancels any outstanding timer tasks.
     */
    private void cancelTimer() {
        ThreadUtils.checkIsOnMainThread();
        Log.d(TAG, "cancelTimer");
        handler.removeCallbacks(bluetoothTimeoutRunnable);
    }

    /**
     * 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() {
        ThreadUtils.checkIsOnMainThread();
        if (bluetoothState == State.UNINITIALIZED || bluetoothHeadset == null) {
            return;
        }
        Log.d(TAG, "bluetoothTimeout: BT state=" + bluetoothState + ", " + "attempts: " + scoConnectionAttempts + ", " + "SCO is on: " + isSreplaced());
        if (bluetoothState != State.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, "SCO connected with " + bluetoothDevice.getName());
                scoConnected = true;
            } else {
                Log.d(TAG, "SCO is not connected with " + bluetoothDevice.getName());
            }
        }
        if (scoConnected) {
            // We thought BT had timed out, but it's actually on; updating state.
            bluetoothState = State.SCO_CONNECTED;
            scoConnectionAttempts = 0;
        } else {
            // Give up and "cancel" our request by calling stopBluetoothSco().
            Log.w(TAG, "BT failed to connect after timeout");
            stopScoAudio();
        }
        updateAudioDeviceState();
        Log.d(TAG, "bluetoothTimeout done: BT state=" + bluetoothState);
    }

    /**
     * Checks whether audio uses Bluetooth SCO.
     */
    private boolean isSreplaced() {
        return audioManager.isBluetoothSreplaced();
    }

    /**
     * Converts BluetoothAdapter states into local string representations.
     */
    private String stateToString(int state) {
        switch(state) {
            case BluetoothAdapter.STATE_DISCONNECTED:
                return "DISCONNECTED";
            case BluetoothAdapter.STATE_CONNECTED:
                return "CONNECTED";
            case BluetoothAdapter.STATE_CONNECTING:
                return "CONNECTING";
            case BluetoothAdapter.STATE_DISCONNECTING:
                return "DISCONNECTING";
            case BluetoothAdapter.STATE_OFF:
                return "OFF";
            case BluetoothAdapter.STATE_ON:
                return "ON";
            case BluetoothAdapter.STATE_TURNING_OFF:
                // 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
                // for STATE_ON before attempting to use the adapter.
                return "TURNING_ON";
            default:
                return "INVALID";
        }
    }
}

17 Source : BluetoothProfileServiceTemplate.java
with Apache License 2.0
from devyok

protected boolean disconnect2(BluetoothDevice device, BluetoothProfile profile) {
    if (this.profileType == BluetoothProfile.A2DP) {
        BluetoothA2dp bluetoothA2dp = (BluetoothA2dp) profile;
        return bluetoothA2dp.disconnect(device);
    } else if (this.profileType == BluetoothProfile.HEADSET) {
        BluetoothHeadset bluetoothHeadset = (BluetoothHeadset) profile;
        return bluetoothHeadset.disconnect(device);
    }
    return false;
}

17 Source : BluetoothProfileServiceTemplate.java
with Apache License 2.0
from devyok

protected int getPriority2(BluetoothDevice device, BluetoothProfile profile) {
    if (this.profileType == BluetoothProfile.A2DP) {
        BluetoothA2dp bluetoothA2dp = (BluetoothA2dp) profile;
        return bluetoothA2dp.getPriority(device);
    } else if (this.profileType == BluetoothProfile.HEADSET) {
        BluetoothHeadset bluetoothHeadset = (BluetoothHeadset) profile;
        return bluetoothHeadset.getPriority(device);
    }
    return -1;
}

16 Source : AppRTCBluetoothManager.java
with ISC License
from zxcpoiu

/**
 * AppRTCProximitySensor manages functions related to Bluetoth devices in the
 * AppRTC demo.
 */
public clreplaced AppRTCBluetoothManager {

    private static final String TAG = "AppRTCBluetoothManager";

    // 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;

    // Bluetooth connection state.
    public enum State {

        // Bluetooth is not available; no adapter or Bluetooth is off.
        UNINITIALIZED,
        // Bluetooth error happened when trying to start Bluetooth.
        ERROR,
        // Bluetooth proxy object for the Headset profile exists, but no connected headset devices,
        // SCO is not started or disconnected.
        HEADSET_UNAVAILABLE,
        // Bluetooth proxy object for the Headset profile 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
    }

    private final Context apprtcContext;

    private final InCallManagerModule apprtcAudioManager;

    private final AudioManager audioManager;

    private final Handler handler;

    int scoConnectionAttempts;

    private State bluetoothState;

    private final BluetoothProfile.ServiceListener bluetoothServiceListener;

    private BluetoothAdapter bluetoothAdapter;

    private BluetoothHeadset bluetoothHeadset;

    private BluetoothDevice bluetoothDevice;

    private final BroadcastReceiver bluetoothHeadsetReceiver;

    // 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();
        }
    };

    /**
     * Implementation of an interface that notifies BluetoothProfile IPC clients when they have been
     * connected to or disconnected from the service.
     */
    private clreplaced BluetoothServiceListener implements BluetoothProfile.ServiceListener {

        @Override
        public // connection and perform other operations that are relevant to the headset profile.
        void onServiceConnected(int profile, BluetoothProfile proxy) {
            if (profile != BluetoothProfile.HEADSET || bluetoothState == State.UNINITIALIZED) {
                return;
            }
            Log.d(TAG, "BluetoothServiceListener.onServiceConnected: BT state=" + bluetoothState);
            // Android only supports one connected Bluetooth Headset at a time.
            bluetoothHeadset = (BluetoothHeadset) proxy;
            updateAudioDeviceState();
            Log.d(TAG, "onServiceConnected done: BT state=" + bluetoothState);
        }

        @Override
        public /**
         * Notifies the client when the proxy object has been disconnected from the service.
         */
        void onServiceDisconnected(int profile) {
            if (profile != BluetoothProfile.HEADSET || bluetoothState == State.UNINITIALIZED) {
                return;
            }
            Log.d(TAG, "BluetoothServiceListener.onServiceDisconnected: BT state=" + bluetoothState);
            stopScoAudio();
            bluetoothHeadset = null;
            bluetoothDevice = null;
            bluetoothState = State.HEADSET_UNAVAILABLE;
            updateAudioDeviceState();
            Log.d(TAG, "onServiceDisconnected done: BT state=" + bluetoothState);
        }
    }

    // Intent broadcast receiver which handles changes in Bluetooth device availability.
    // Detects headset changes and Bluetooth SCO state changes.
    private clreplaced BluetoothHeadsetBroadcastReceiver extends BroadcastReceiver {

        @Override
        public void onReceive(Context context, Intent intent) {
            if (bluetoothState == State.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. Typically received when user turns on a BT
            // 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(TAG, "BluetoothHeadsetBroadcastReceiver.onReceive: " + "a=ACTION_CONNECTION_STATE_CHANGED, " + "s=" + stateToString(state) + ", " + "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.
                    stopScoAudio();
                    updateAudioDeviceState();
                }
            // 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(TAG, "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) {
                        Log.d(TAG, "+++ Bluetooth audio SCO is now connected");
                        bluetoothState = State.SCO_CONNECTED;
                        scoConnectionAttempts = 0;
                        updateAudioDeviceState();
                    } else {
                        Log.w(TAG, "Unexpected state BluetoothHeadset.STATE_AUDIO_CONNECTED");
                    }
                } else if (state == BluetoothHeadset.STATE_AUDIO_CONNECTING) {
                    Log.d(TAG, "+++ Bluetooth audio SCO is now connecting...");
                } else if (state == BluetoothHeadset.STATE_AUDIO_DISCONNECTED) {
                    Log.d(TAG, "+++ Bluetooth audio SCO is now disconnected");
                    if (isInitialStickyBroadcast()) {
                        Log.d(TAG, "Ignore STATE_AUDIO_DISCONNECTED initial sticky broadcast.");
                        return;
                    }
                    updateAudioDeviceState();
                }
            }
            Log.d(TAG, "onReceive done: BT state=" + bluetoothState);
        }
    }

    /**
     * Construction.
     */
    public static AppRTCBluetoothManager create(Context context, InCallManagerModule audioManager) {
        Log.d(TAG, "create");
        return new AppRTCBluetoothManager(context, audioManager);
    }

    protected AppRTCBluetoothManager(Context context, InCallManagerModule audioManager) {
        Log.d(TAG, "ctor");
        apprtcContext = context;
        apprtcAudioManager = audioManager;
        this.audioManager = getAudioManager(context);
        bluetoothState = State.UNINITIALIZED;
        bluetoothServiceListener = new BluetoothServiceListener();
        bluetoothHeadsetReceiver = new BluetoothHeadsetBroadcastReceiver();
        handler = new Handler(Looper.getMainLooper());
    }

    /**
     * Returns the internal state.
     */
    public State getState() {
        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.
     */
    public void start() {
        Log.d(TAG, "start");
        if (!hasPermission(apprtcContext, android.Manifest.permission.BLUETOOTH)) {
            Log.w(TAG, "Process (pid=" + Process.myPid() + ") lacks BLUETOOTH permission");
            return;
        }
        if (bluetoothState != State.UNINITIALIZED) {
            Log.w(TAG, "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, "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, "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(apprtcContext, bluetoothServiceListener, BluetoothProfile.HEADSET)) {
            Log.e(TAG, "BluetoothAdapter.getProfileProxy(HEADSET) failed");
            return;
        }
        // Register receivers for BluetoothHeadset change notifications.
        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, "HEADSET profile state: " + stateToString(bluetoothAdapter.getProfileConnectionState(BluetoothProfile.HEADSET)));
        Log.d(TAG, "Bluetooth proxy for headset profile has started");
        bluetoothState = State.HEADSET_UNAVAILABLE;
        Log.d(TAG, "start done: BT state=" + bluetoothState);
    }

    /**
     * Stops and closes all components related to Bluetooth audio.
     */
    public void stop() {
        Log.d(TAG, "stop: BT state=" + bluetoothState);
        if (bluetoothAdapter == null) {
            return;
        }
        // Stop BT SCO connection with remote device if needed.
        stopScoAudio();
        // Close down remaining BT resources.
        if (bluetoothState == State.UNINITIALIZED) {
            return;
        }
        unregisterReceiver(bluetoothHeadsetReceiver);
        cancelTimer();
        if (bluetoothHeadset != null) {
            bluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, bluetoothHeadset);
            bluetoothHeadset = null;
        }
        bluetoothAdapter = null;
        bluetoothDevice = null;
        bluetoothState = State.UNINITIALIZED;
        Log.d(TAG, "stop done: BT state=" + bluetoothState);
    }

    /**
     * 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() {
        Log.d(TAG, "startSco: BT state=" + bluetoothState + ", " + "attempts: " + scoConnectionAttempts + ", " + "SCO is on: " + isSreplaced());
        if (scoConnectionAttempts >= MAX_SCO_CONNECTION_ATTEMPTS) {
            Log.e(TAG, "BT SCO connection fails - no more attempts");
            return false;
        }
        if (bluetoothState != State.HEADSET_AVAILABLE) {
            Log.e(TAG, "BT SCO connection fails - no headset available");
            return false;
        }
        // Start BT SCO channel and wait for ACTION_AUDIO_STATE_CHANGED.
        Log.d(TAG, "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.
        bluetoothState = State.SCO_CONNECTING;
        audioManager.startBluetoothSco();
        audioManager.setBluetoothSreplaced(true);
        scoConnectionAttempts++;
        startTimer();
        Log.d(TAG, "startScoAudio done: BT state=" + bluetoothState + ", " + "SCO is on: " + isSreplaced());
        return true;
    }

    /**
     * Stops Bluetooth SCO connection with remote device.
     */
    public void stopScoAudio() {
        Log.d(TAG, "stopScoAudio: BT state=" + bluetoothState + ", " + "SCO is on: " + isSreplaced());
        if (bluetoothState != State.SCO_CONNECTING && bluetoothState != State.SCO_CONNECTED) {
            return;
        }
        cancelTimer();
        audioManager.stopBluetoothSco();
        audioManager.setBluetoothSreplaced(false);
        bluetoothState = State.SCO_DISCONNECTING;
        Log.d(TAG, "stopScoAudio done: BT state=" + bluetoothState + ", " + "SCO is on: " + isSreplaced());
    }

    /**
     * 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.
     */
    public void updateDevice() {
        if (bluetoothState == State.UNINITIALIZED || bluetoothHeadset == null) {
            return;
        }
        Log.d(TAG, "updateDevice");
        // Get connected devices for the headset profile. Returns the set of
        // devices which are in state STATE_CONNECTED. The BluetoothDevice clreplaced
        // is just a thin wrapper for a Bluetooth hardware address.
        List<BluetoothDevice> devices = bluetoothHeadset.getConnectedDevices();
        if (devices.isEmpty()) {
            bluetoothDevice = null;
            bluetoothState = State.HEADSET_UNAVAILABLE;
            Log.d(TAG, "No connected bluetooth headset");
        } else {
            // Always use first device in list. Android only supports one device.
            bluetoothDevice = devices.get(0);
            bluetoothState = State.HEADSET_AVAILABLE;
            Log.d(TAG, "Connected bluetooth headset: " + "name=" + bluetoothDevice.getName() + ", " + "state=" + stateToString(bluetoothHeadset.getConnectionState(bluetoothDevice)) + ", SCO audio=" + bluetoothHeadset.isAudioConnected(bluetoothDevice));
        }
        Log.d(TAG, "updateDevice done: BT state=" + bluetoothState);
    }

    /**
     * Stubs for test mocks.
     */
    protected AudioManager getAudioManager(Context context) {
        return (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
    }

    protected void registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
        apprtcContext.registerReceiver(receiver, filter);
    }

    protected void unregisterReceiver(BroadcastReceiver receiver) {
        apprtcContext.unregisterReceiver(receiver);
    }

    protected boolean getBluetoothProfileProxy(Context context, BluetoothProfile.ServiceListener listener, int profile) {
        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(TAG, "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(TAG, "paired devices:");
            for (BluetoothDevice device : pairedDevices) {
                Log.d(TAG, " name=" + device.getName() + ", address=" + device.getAddress());
            }
        }
    }

    /**
     * Ensures that the audio manager updates its list of available audio devices.
     */
    private void updateAudioDeviceState() {
        Log.d(TAG, "updateAudioDeviceState");
        apprtcAudioManager.updateAudioDeviceState();
    }

    /**
     * Starts timer which times out after BLUETOOTH_SCO_TIMEOUT_MS milliseconds.
     */
    private void startTimer() {
        Log.d(TAG, "startTimer");
        handler.postDelayed(bluetoothTimeoutRunnable, BLUETOOTH_SCO_TIMEOUT_MS);
    }

    /**
     * Cancels any outstanding timer tasks.
     */
    private void cancelTimer() {
        Log.d(TAG, "cancelTimer");
        handler.removeCallbacks(bluetoothTimeoutRunnable);
    }

    /**
     * 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 (bluetoothState == State.UNINITIALIZED || bluetoothHeadset == null) {
            return;
        }
        Log.d(TAG, "bluetoothTimeout: BT state=" + bluetoothState + ", " + "attempts: " + scoConnectionAttempts + ", " + "SCO is on: " + isSreplaced());
        if (bluetoothState != State.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, "SCO connected with " + bluetoothDevice.getName());
                scoConnected = true;
            } else {
                Log.d(TAG, "SCO is not connected with " + bluetoothDevice.getName());
            }
        }
        if (scoConnected) {
            // We thought BT had timed out, but it's actually on; updating state.
            bluetoothState = State.SCO_CONNECTED;
            scoConnectionAttempts = 0;
        } else {
            // Give up and "cancel" our request by calling stopBluetoothSco().
            Log.w(TAG, "BT failed to connect after timeout");
            stopScoAudio();
        }
        updateAudioDeviceState();
        Log.d(TAG, "bluetoothTimeout done: BT state=" + bluetoothState);
    }

    /**
     * Checks whether audio uses Bluetooth SCO.
     */
    private boolean isSreplaced() {
        return audioManager.isBluetoothSreplaced();
    }

    /**
     * Converts BluetoothAdapter states into local string representations.
     */
    private String stateToString(int state) {
        switch(state) {
            case BluetoothAdapter.STATE_DISCONNECTED:
                return "DISCONNECTED";
            case BluetoothAdapter.STATE_CONNECTED:
                return "CONNECTED";
            case BluetoothAdapter.STATE_CONNECTING:
                return "CONNECTING";
            case BluetoothAdapter.STATE_DISCONNECTING:
                return "DISCONNECTING";
            case BluetoothAdapter.STATE_OFF:
                return "OFF";
            case BluetoothAdapter.STATE_ON:
                return "ON";
            case BluetoothAdapter.STATE_TURNING_OFF:
                // 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
                // for STATE_ON before attempting to use the adapter.
                return "TURNING_ON";
            default:
                return "INVALID";
        }
    }
}

16 Source : VoipBluetoothManager.java
with GNU Affero General Public License v3.0
from threema-ch

/**
 * VoipBluetoothManager manages functions related to Bluetoth devices in
 * Threema voice calls.
 */
public clreplaced VoipBluetoothManager {

    private static final Logger logger = LoggerFactory.getLogger(VoipBluetoothManager.clreplaced);

    // 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;

    // Bluetooth connection state.
    public enum State {

        // Bluetooth is not available; no adapter or Bluetooth is off.
        UNINITIALIZED,
        // Bluetooth error happened when trying to start Bluetooth.
        ERROR,
        // Bluetooth proxy object for the Headset profile exists, but no connected headset devices,
        // SCO is not started or disconnected.
        HEADSET_UNAVAILABLE,
        // Bluetooth proxy object for the Headset profile 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
    }

    private final Context apprtcContext;

    private final VoipAudioManager voipAudioManager;

    private final AudioManager audioManager;

    private final Handler handler;

    int scoConnectionAttempts;

    private State bluetoothState;

    private Long bluetoothAudioConnectedAt;

    private final BluetoothProfile.ServiceListener bluetoothServiceListener;

    private BluetoothAdapter bluetoothAdapter;

    private BluetoothHeadset bluetoothHeadset;

    private BluetoothDevice bluetoothDevice;

    private final BroadcastReceiver bluetoothHeadsetReceiver;

    // 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();
        }
    };

    /**
     * Implementation of an interface that notifies BluetoothProfile IPC clients when they have been
     * connected to or disconnected from the service.
     */
    private clreplaced BluetoothServiceListener implements BluetoothProfile.ServiceListener {

        @Override
        public // connection and perform other operations that are relevant to the headset profile.
        void onServiceConnected(int profile, BluetoothProfile proxy) {
            if (profile != BluetoothProfile.HEADSET || bluetoothState == State.UNINITIALIZED) {
                return;
            }
            logger.debug("BluetoothServiceListener.onServiceConnected: BT state=" + bluetoothState);
            // Android only supports one connected Bluetooth Headset at a time.
            bluetoothHeadset = (BluetoothHeadset) proxy;
            updateAudioDeviceState();
            logger.debug("onServiceConnected done: BT state=" + bluetoothState);
        }

        @Override
        public /**
         * Notifies the client when the proxy object has been disconnected from the service.
         */
        void onServiceDisconnected(int profile) {
            if (profile != BluetoothProfile.HEADSET || bluetoothState == State.UNINITIALIZED) {
                return;
            }
            logger.debug("BluetoothServiceListener.onServiceDisconnected: BT state=" + bluetoothState);
            stopScoAudio();
            bluetoothHeadset = null;
            bluetoothDevice = null;
            bluetoothState = State.HEADSET_UNAVAILABLE;
            updateAudioDeviceState();
            logger.debug("onServiceDisconnected done: BT state=" + bluetoothState);
        }
    }

    // Intent broadcast receiver which handles changes in Bluetooth device availability.
    // Detects headset changes and Bluetooth SCO state changes.
    private clreplaced BluetoothHeadsetBroadcastReceiver extends BroadcastReceiver {

        @Override
        public void onReceive(Context context, Intent intent) {
            if (bluetoothState == State.UNINITIALIZED) {
                return;
            }
            final String action = intent.getAction();
            if (action.equals(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED)) {
                this.onConnectionStateChange(intent);
            } else if (action.equals(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED)) {
                this.onAudioStateChange(intent);
            } else {
                logger.warn("Unknown bluetooth broadcast action: {}", action);
            }
            logger.debug("onReceive done: BT state={}", bluetoothState);
        }

        /**
         * 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. Typically received when user turns on a BT
         * headset while audio is active using another audio device.
         */
        private void onConnectionStateChange(Intent intent) {
            final int state = intent.getIntExtra(BluetoothHeadset.EXTRA_STATE, BluetoothHeadset.STATE_DISCONNECTED);
            logger.debug("BluetoothHeadsetBroadcastReceiver.onReceive: " + "a=ACTION_CONNECTION_STATE_CHANGED, " + "s=" + headsetStateToString(state) + ", " + "sb=" + isInitialStickyBroadcast() + ", " + "BT state: " + bluetoothState);
            switch(state) {
                case BluetoothHeadset.STATE_CONNECTED:
                    scoConnectionAttempts = 0;
                    updateAudioDeviceState();
                    break;
                case BluetoothHeadset.STATE_CONNECTING:
                case BluetoothHeadset.STATE_DISCONNECTING:
                    // No action needed
                    break;
                case BluetoothHeadset.STATE_DISCONNECTED:
                    // Bluetooth is probably powered off during the call.
                    stopScoAudio();
                    updateAudioDeviceState();
                    break;
            }
        }

        /**
         * Change in the audio (SCO) connection state of the Headset profile.
         * Typically received after call to startScoAudio() has finalized.
         */
        private void onAudioStateChange(Intent intent) {
            final int state = intent.getIntExtra(BluetoothHeadset.EXTRA_STATE, BluetoothHeadset.STATE_AUDIO_DISCONNECTED);
            logger.debug("BluetoothHeadsetBroadcastReceiver.onReceive: " + "a=ACTION_AUDIO_STATE_CHANGED, " + "s=" + headsetStateToString(state) + ", " + "sb=" + isInitialStickyBroadcast() + ", " + "BT state: " + bluetoothState);
            // Switch BluetoothHeadsetBroadcastReceiver.onReceive: a=ACTION_AUDIO_STATE_CHANGED, s=A_DISCONNECTED, sb=false, BT state: HEADSET_AVAILABLE
            // Btn BluetoothHeadsetBroadcastReceiver.onReceive: a=ACTION_AUDIO_STATE_CHANGED, s=A_DISCONNECTED, sb=false, BT state: SCO_CONNECTED
            if (state == BluetoothHeadset.STATE_AUDIO_CONNECTED) {
                cancelTimer();
                if (bluetoothState == State.SCO_CONNECTING) {
                    logger.debug("+++ Bluetooth audio SCO is now connected");
                    bluetoothState = State.SCO_CONNECTED;
                    bluetoothAudioConnectedAt = System.nanoTime();
                    scoConnectionAttempts = 0;
                    updateAudioDeviceState();
                } else {
                    logger.warn("Unexpected state BluetoothHeadset.STATE_AUDIO_CONNECTED");
                }
            } else if (state == BluetoothHeadset.STATE_AUDIO_CONNECTING) {
                logger.debug("+++ Bluetooth audio SCO is now connecting...");
            } else if (state == BluetoothHeadset.STATE_AUDIO_DISCONNECTED) {
                logger.debug("+++ Bluetooth audio SCO is now disconnected");
                if (isInitialStickyBroadcast()) {
                    logger.debug("Ignore STATE_AUDIO_DISCONNECTED initial sticky broadcast.");
                } else if (bluetoothState == State.SCO_CONNECTED) {
                    // The bluetooth device is now disconnected after previously having been
                    // connected. We now have two options: Either switch audio to the built-in
                    // speaker / earpiece, or end the call.
                    // 
                    // Since many bluetooth headsets just have one button, and because the user
                    // should be able to hang up the call with that, we'll end the call.
                    // 
                    // Note that some bluetooth devices connect the audio for a very short while
                    // and then disconnect it again. This is probably if Android would allow audio
                    // connections, but it's turned off inside the device. (This happens e.g. on
                    // the Asus ZenWatch devices when bluetooth audio is turned off.) In that
                    // case we simply stop bluetooth.
                    // 
                    // There are also headsets that detect whether they are being worn or not.
                    // Based on the data from support tickets, this process takes roughly a second.
                    // Therefore we set the threshold at 1500 ms to avoid ending the call if the
                    // headset disconnects after 1050 ms.
                    Long msElapsed = null;
                    long msElapsedThreshold = 1500;
                    if (bluetoothAudioConnectedAt != null) {
                        msElapsed = (System.nanoTime() - bluetoothAudioConnectedAt) / 1000 / 1000;
                    }
                    logger.info("Time elapsed since bluetooth audio connected: {} ms", msElapsed);
                    if (msElapsed == null || msElapsed < msElapsedThreshold) {
                        logger.info("Bluetooth headset disconnected. Switching to phone audio.");
                        VoipBluetoothManager.this.stop();
                        VoipBluetoothManager.this.updateAudioDeviceState();
                    } else {
                        logger.info("Bluetooth headset disconnected after {} ms. Ending call.", msElapsed);
                        VoipUtil.sendVoipCommand(ThreemaApplication.getAppContext(), VoipCallService.clreplaced, VoipCallService.ACTION_HANGUP);
                    }
                } else {
                    // The output device was probably switched via UI
                    VoipBluetoothManager.this.updateAudioDeviceState();
                }
            }
        }
    }

    /**
     * Construction.
     */
    static VoipBluetoothManager create(Context context, VoipAudioManager audioManager) {
        logger.debug("create" + AppRTCUtils.getThreadInfo());
        return new VoipBluetoothManager(context, audioManager);
    }

    protected VoipBluetoothManager(Context context, VoipAudioManager audioManager) {
        logger.debug("ctor");
        ThreadUtils.checkIsOnMainThread();
        this.apprtcContext = context;
        this.voipAudioManager = audioManager;
        this.audioManager = getAudioManager(context);
        this.bluetoothState = State.UNINITIALIZED;
        this.bluetoothServiceListener = new BluetoothServiceListener();
        this.bluetoothHeadsetReceiver = new BluetoothHeadsetBroadcastReceiver();
        this.handler = new Handler(Looper.getMainLooper());
    }

    /**
     * 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.
     */
    public void start() {
        ThreadUtils.checkIsOnMainThread();
        logger.debug("start");
        if (!hasPermission(apprtcContext, android.Manifest.permission.BLUETOOTH)) {
            logger.warn("Process (pid=" + Process.myPid() + ") lacks BLUETOOTH permission");
            return;
        }
        if (this.bluetoothState != State.UNINITIALIZED) {
            logger.warn("Invalid BT state");
            return;
        }
        this.bluetoothHeadset = null;
        this.bluetoothDevice = null;
        this.scoConnectionAttempts = 0;
        // Get a handle to the default local Bluetooth adapter.
        this.bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
        if (this.bluetoothAdapter == null) {
            logger.warn("Device does not support Bluetooth");
            return;
        }
        // Ensure that the device supports use of BT SCO audio for off call use cases.
        if (!this.audioManager.isBluetoothScoAvailableOffCall()) {
            logger.error("Bluetooth SCO audio is not available off call");
            return;
        }
        this.logBluetoothAdapterInfo(bluetoothAdapter);
        // Establish a connection to the HEADSET profile (includes both Bluetooth Headset and
        // Hands-Free) proxy object and install a listener.
        if (!this.getBluetoothProfileProxy(this.apprtcContext, this.bluetoothServiceListener, BluetoothProfile.HEADSET)) {
            logger.error("BluetoothAdapter.getProfileProxy(HEADSET) failed");
            return;
        }
        // Register receivers for BluetoothHeadset change notifications.
        final 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);
        logger.debug("HEADSET profile state: " + headsetStateToString(bluetoothAdapter.getProfileConnectionState(BluetoothProfile.HEADSET)));
        logger.debug("Bluetooth proxy for headset profile has started");
        this.bluetoothState = State.HEADSET_UNAVAILABLE;
        logger.debug("start done: BT state=" + bluetoothState);
    }

    /**
     * Stops and closes all components related to Bluetooth audio.
     */
    public void stop() {
        ThreadUtils.checkIsOnMainThread();
        unregisterReceiver(bluetoothHeadsetReceiver);
        logger.debug("stop: BT state=" + bluetoothState);
        if (bluetoothAdapter != null) {
            // Stop BT SCO connection with remote device if needed.
            stopScoAudio();
            // Close down remaining BT resources.
            if (bluetoothState != State.UNINITIALIZED) {
                cancelTimer();
                if (bluetoothHeadset != null) {
                    bluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, bluetoothHeadset);
                    bluetoothHeadset = null;
                }
                bluetoothAdapter = null;
                bluetoothDevice = null;
                bluetoothState = State.UNINITIALIZED;
            }
        }
        logger.debug("stop done: BT state=" + bluetoothState);
    }

    /**
     * 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();
        logger.debug("startSco: BT state=" + bluetoothState + ", " + "attempts: " + scoConnectionAttempts + ", " + "SCO is on: " + isSreplaced());
        if (scoConnectionAttempts >= MAX_SCO_CONNECTION_ATTEMPTS) {
            logger.error("BT SCO connection fails - no more attempts");
            return false;
        }
        if (bluetoothState != State.HEADSET_AVAILABLE) {
            logger.error("BT SCO connection fails - no headset available");
            return false;
        }
        // Start BT SCO channel and wait for ACTION_AUDIO_STATE_CHANGED.
        logger.debug("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.
        this.bluetoothState = State.SCO_CONNECTING;
        try {
            this.audioManager.startBluetoothSco();
        } catch (RuntimeException e) {
            logger.error("Could not start bluetooth SCO", e);
        }
        this.scoConnectionAttempts++;
        this.startTimer();
        logger.debug("startScoAudio done: BT state=" + bluetoothState);
        return true;
    }

    /**
     * Stops Bluetooth SCO connection with remote device.
     */
    public void stopScoAudio() {
        ThreadUtils.checkIsOnMainThread();
        logger.debug("stopScoAudio: BT state=" + bluetoothState + ", " + "SCO is on: " + isSreplaced());
        if (bluetoothState != State.SCO_CONNECTING && bluetoothState != State.SCO_CONNECTED) {
            return;
        }
        cancelTimer();
        audioManager.stopBluetoothSco();
        bluetoothState = State.SCO_DISCONNECTING;
        logger.debug("stopScoAudio done: BT state=" + bluetoothState);
    }

    /**
     * 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.
     */
    public void updateDevice() {
        if (bluetoothState == State.UNINITIALIZED || bluetoothHeadset == null) {
            return;
        }
        logger.debug("updateDevice");
        // Get connected devices for the headset profile. Returns the set of
        // devices which are in state STATE_CONNECTED. The BluetoothDevice clreplaced
        // is just a thin wrapper for a Bluetooth hardware address.
        List<BluetoothDevice> devices = bluetoothHeadset.getConnectedDevices();
        if (devices.isEmpty()) {
            bluetoothDevice = null;
            bluetoothState = State.HEADSET_UNAVAILABLE;
            logger.debug("No connected bluetooth headset");
        } else {
            // Always use first device is list. Android only supports one device.
            bluetoothDevice = devices.get(0);
            bluetoothState = State.HEADSET_AVAILABLE;
            logger.debug("Connected bluetooth headset: " + "name=" + bluetoothDevice.getName() + ", " + "state=" + headsetStateToString(bluetoothHeadset.getConnectionState(bluetoothDevice)) + ", SCO audio=" + bluetoothHeadset.isAudioConnected(bluetoothDevice));
        }
        logger.debug("updateDevice done: BT state=" + bluetoothState);
    }

    /**
     * Stubs for test mocks.
     */
    protected AudioManager getAudioManager(Context context) {
        return (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
    }

    protected void registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
        apprtcContext.registerReceiver(receiver, filter);
    }

    protected void unregisterReceiver(BroadcastReceiver receiver) {
        try {
            apprtcContext.unregisterReceiver(receiver);
        } catch (Exception e) {
            // receiver not registered
            logger.error("Could not unregister receiver", e);
        }
    }

    protected boolean getBluetoothProfileProxy(Context context, BluetoothProfile.ServiceListener listener, int profile) {
        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) {
        logger.debug("BluetoothAdapter: " + "enabled=" + localAdapter.isEnabled() + ", " + "state=" + adapterStateToString(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()) {
            logger.debug("paired devices:");
            for (BluetoothDevice device : pairedDevices) {
                logger.debug(" name=" + device.getName() + ", address=" + device.getAddress());
            }
        }
    }

    /**
     * Ensures that the audio manager updates its list of available audio devices.
     */
    private void updateAudioDeviceState() {
        ThreadUtils.checkIsOnMainThread();
        logger.debug("updateAudioDeviceState");
        voipAudioManager.updateAudioDeviceState();
    }

    /**
     * Starts timer which times out after BLUETOOTH_SCO_TIMEOUT_MS milliseconds.
     */
    private void startTimer() {
        ThreadUtils.checkIsOnMainThread();
        logger.debug("startTimer");
        handler.postDelayed(bluetoothTimeoutRunnable, BLUETOOTH_SCO_TIMEOUT_MS);
    }

    /**
     * Cancels any outstanding timer tasks.
     */
    private void cancelTimer() {
        ThreadUtils.checkIsOnMainThread();
        logger.debug("cancelTimer");
        handler.removeCallbacks(bluetoothTimeoutRunnable);
    }

    /**
     * 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() {
        ThreadUtils.checkIsOnMainThread();
        if (bluetoothState == State.UNINITIALIZED || bluetoothHeadset == null) {
            return;
        }
        logger.debug("bluetoothTimeout: BT state=" + bluetoothState + ", " + "attempts: " + scoConnectionAttempts + ", " + "SCO is on: " + isSreplaced());
        if (bluetoothState != State.SCO_CONNECTING) {
            return;
        }
        // Bluetooth SCO should be connecting; check the latest result.
        boolean scoConnected = false;
        final List<BluetoothDevice> devices = bluetoothHeadset.getConnectedDevices();
        if (devices.size() > 0) {
            bluetoothDevice = devices.get(0);
            if (bluetoothHeadset.isAudioConnected(bluetoothDevice)) {
                logger.debug("SCO connected with " + bluetoothDevice.getName());
                scoConnected = true;
            } else {
                logger.debug("SCO is not connected with " + bluetoothDevice.getName());
            }
        }
        if (scoConnected) {
            // We thought BT had timed out, but it's actually on; updating state.
            bluetoothState = State.SCO_CONNECTED;
            scoConnectionAttempts = 0;
        } else {
            // Give up and "cancel" our request by calling stopBluetoothSco().
            logger.warn("BT failed to connect after timeout");
            stopScoAudio();
        }
        updateAudioDeviceState();
        logger.debug("bluetoothTimeout done: BT state=" + bluetoothState);
    }

    /**
     * Checks whether audio uses Bluetooth SCO.
     */
    private boolean isSreplaced() {
        return audioManager.isBluetoothSreplaced();
    }

    /**
     * Converts BluetoothAdapter states into local string representations.
     */
    private String adapterStateToString(int state) {
        switch(state) {
            case BluetoothAdapter.STATE_DISCONNECTED:
                return "DISCONNECTED";
            case BluetoothAdapter.STATE_CONNECTED:
                return "CONNECTED";
            case BluetoothAdapter.STATE_CONNECTING:
                return "CONNECTING";
            case BluetoothAdapter.STATE_DISCONNECTING:
                return "DISCONNECTING";
            case BluetoothAdapter.STATE_OFF:
                return "OFF";
            case BluetoothAdapter.STATE_ON:
                return "ON";
            case BluetoothAdapter.STATE_TURNING_OFF:
                // 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
                // for STATE_ON before attempting to use the adapter.
                return "TURNING_ON";
            default:
                return "INVALID";
        }
    }

    /**
     * Converts BluetoothHeadset states into local string representations.
     */
    private String headsetStateToString(int state) {
        switch(state) {
            case BluetoothHeadset.STATE_CONNECTING:
                return "CONNECTING";
            case BluetoothHeadset.STATE_AUDIO_CONNECTING:
                return "A_CONNECTING";
            case BluetoothHeadset.STATE_CONNECTED:
                return "CONNECTED";
            case BluetoothHeadset.STATE_AUDIO_CONNECTED:
                return "A_CONNECTED";
            case BluetoothHeadset.STATE_DISCONNECTING:
                return "DISCONNECTING";
            case BluetoothHeadset.STATE_DISCONNECTED:
                return "DISCONNECTED";
            case BluetoothHeadset.STATE_AUDIO_DISCONNECTED:
                return "A_DISCONNECTED";
            default:
                return "INVALID_STATE";
        }
    }
}

16 Source : AppRTCBluetoothManager.java
with MIT License
from pcgpcgpcg

/**
 * AppRTCProximitySensor manages functions related to Bluetoth devices in the
 * AppRTC demo.
 */
public clreplaced AppRTCBluetoothManager {

    private static final String TAG = "AppRTCBluetoothManager";

    // 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;

    // Bluetooth connection state.
    public enum State {

        // Bluetooth is not available; no adapter or Bluetooth is off.
        UNINITIALIZED,
        // Bluetooth error happened when trying to start Bluetooth.
        ERROR,
        // Bluetooth proxy object for the Headset profile exists, but no connected headset devices,
        // SCO is not started or disconnected.
        HEADSET_UNAVAILABLE,
        // Bluetooth proxy object for the Headset profile 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
    }

    private final Context apprtcContext;

    private final AppRTCAudioManager apprtcAudioManager;

    @Nullable
    private final AudioManager audioManager;

    private final Handler handler;

    int scoConnectionAttempts;

    private State bluetoothState;

    private final BluetoothProfile.ServiceListener bluetoothServiceListener;

    @Nullable
    private BluetoothAdapter bluetoothAdapter;

    @Nullable
    private BluetoothHeadset bluetoothHeadset;

    @Nullable
    private BluetoothDevice bluetoothDevice;

    private final BroadcastReceiver bluetoothHeadsetReceiver;

    // 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();
        }
    };

    /**
     * Implementation of an interface that notifies BluetoothProfile IPC clients when they have been
     * connected to or disconnected from the service.
     */
    private clreplaced BluetoothServiceListener implements BluetoothProfile.ServiceListener {

        @Override
        public // connection and perform other operations that are relevant to the headset profile.
        void onServiceConnected(int profile, BluetoothProfile proxy) {
            if (profile != BluetoothProfile.HEADSET || bluetoothState == State.UNINITIALIZED) {
                return;
            }
            Log.d(TAG, "BluetoothServiceListener.onServiceConnected: BT state=" + bluetoothState);
            // Android only supports one connected Bluetooth Headset at a time.
            bluetoothHeadset = (BluetoothHeadset) proxy;
            updateAudioDeviceState();
            Log.d(TAG, "onServiceConnected done: BT state=" + bluetoothState);
        }

        @Override
        public /**
         * Notifies the client when the proxy object has been disconnected from the service.
         */
        void onServiceDisconnected(int profile) {
            if (profile != BluetoothProfile.HEADSET || bluetoothState == State.UNINITIALIZED) {
                return;
            }
            Log.d(TAG, "BluetoothServiceListener.onServiceDisconnected: BT state=" + bluetoothState);
            stopScoAudio();
            bluetoothHeadset = null;
            bluetoothDevice = null;
            bluetoothState = State.HEADSET_UNAVAILABLE;
            updateAudioDeviceState();
            Log.d(TAG, "onServiceDisconnected done: BT state=" + bluetoothState);
        }
    }

    // Intent broadcast receiver which handles changes in Bluetooth device availability.
    // Detects headset changes and Bluetooth SCO state changes.
    private clreplaced BluetoothHeadsetBroadcastReceiver extends BroadcastReceiver {

        @Override
        public void onReceive(Context context, Intent intent) {
            if (bluetoothState == State.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. Typically received when user turns on a BT
            // 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(TAG, "BluetoothHeadsetBroadcastReceiver.onReceive: " + "a=ACTION_CONNECTION_STATE_CHANGED, " + "s=" + stateToString(state) + ", " + "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.
                    stopScoAudio();
                    updateAudioDeviceState();
                }
            // 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(TAG, "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) {
                        Log.d(TAG, "+++ Bluetooth audio SCO is now connected");
                        bluetoothState = State.SCO_CONNECTED;
                        scoConnectionAttempts = 0;
                        updateAudioDeviceState();
                    } else {
                        Log.w(TAG, "Unexpected state BluetoothHeadset.STATE_AUDIO_CONNECTED");
                    }
                } else if (state == BluetoothHeadset.STATE_AUDIO_CONNECTING) {
                    Log.d(TAG, "+++ Bluetooth audio SCO is now connecting...");
                } else if (state == BluetoothHeadset.STATE_AUDIO_DISCONNECTED) {
                    Log.d(TAG, "+++ Bluetooth audio SCO is now disconnected");
                    if (isInitialStickyBroadcast()) {
                        Log.d(TAG, "Ignore STATE_AUDIO_DISCONNECTED initial sticky broadcast.");
                        return;
                    }
                    updateAudioDeviceState();
                }
            }
            Log.d(TAG, "onReceive done: BT state=" + bluetoothState);
        }
    }

    /**
     * Construction.
     */
    static AppRTCBluetoothManager create(Context context, AppRTCAudioManager audioManager) {
        Log.d(TAG, "create" + AppRTCUtils.getThreadInfo());
        return new AppRTCBluetoothManager(context, audioManager);
    }

    protected AppRTCBluetoothManager(Context context, AppRTCAudioManager audioManager) {
        Log.d(TAG, "ctor");
        ThreadUtils.checkIsOnMainThread();
        apprtcContext = context;
        apprtcAudioManager = audioManager;
        this.audioManager = getAudioManager(context);
        bluetoothState = State.UNINITIALIZED;
        bluetoothServiceListener = new BluetoothServiceListener();
        bluetoothHeadsetReceiver = new BluetoothHeadsetBroadcastReceiver();
        handler = new Handler(Looper.getMainLooper());
    }

    /**
     * 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.
     */
    public void start() {
        ThreadUtils.checkIsOnMainThread();
        Log.d(TAG, "start");
        if (!hasPermission(apprtcContext, android.Manifest.permission.BLUETOOTH)) {
            Log.w(TAG, "Process (pid=" + Process.myPid() + ") lacks BLUETOOTH permission");
            return;
        }
        if (bluetoothState != State.UNINITIALIZED) {
            Log.w(TAG, "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, "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, "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(apprtcContext, bluetoothServiceListener, BluetoothProfile.HEADSET)) {
            Log.e(TAG, "BluetoothAdapter.getProfileProxy(HEADSET) failed");
            return;
        }
        // Register receivers for BluetoothHeadset change notifications.
        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, "HEADSET profile state: " + stateToString(bluetoothAdapter.getProfileConnectionState(BluetoothProfile.HEADSET)));
        Log.d(TAG, "Bluetooth proxy for headset profile has started");
        bluetoothState = State.HEADSET_UNAVAILABLE;
        Log.d(TAG, "start done: BT state=" + bluetoothState);
    }

    /**
     * Stops and closes all components related to Bluetooth audio.
     */
    public void stop() {
        ThreadUtils.checkIsOnMainThread();
        Log.d(TAG, "stop: BT state=" + bluetoothState);
        if (bluetoothAdapter == null) {
            return;
        }
        // Stop BT SCO connection with remote device if needed.
        stopScoAudio();
        // Close down remaining BT resources.
        if (bluetoothState == State.UNINITIALIZED) {
            return;
        }
        unregisterReceiver(bluetoothHeadsetReceiver);
        cancelTimer();
        if (bluetoothHeadset != null) {
            bluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, bluetoothHeadset);
            bluetoothHeadset = null;
        }
        bluetoothAdapter = null;
        bluetoothDevice = null;
        bluetoothState = State.UNINITIALIZED;
        Log.d(TAG, "stop done: BT state=" + bluetoothState);
    }

    /**
     * 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(TAG, "startSco: BT state=" + bluetoothState + ", " + "attempts: " + scoConnectionAttempts + ", " + "SCO is on: " + isSreplaced());
        if (scoConnectionAttempts >= MAX_SCO_CONNECTION_ATTEMPTS) {
            Log.e(TAG, "BT SCO connection fails - no more attempts");
            return false;
        }
        if (bluetoothState != State.HEADSET_AVAILABLE) {
            Log.e(TAG, "BT SCO connection fails - no headset available");
            return false;
        }
        // Start BT SCO channel and wait for ACTION_AUDIO_STATE_CHANGED.
        Log.d(TAG, "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.
        bluetoothState = State.SCO_CONNECTING;
        audioManager.startBluetoothSco();
        audioManager.setBluetoothSreplaced(true);
        scoConnectionAttempts++;
        startTimer();
        Log.d(TAG, "startScoAudio done: BT state=" + bluetoothState + ", " + "SCO is on: " + isSreplaced());
        return true;
    }

    /**
     * Stops Bluetooth SCO connection with remote device.
     */
    public void stopScoAudio() {
        ThreadUtils.checkIsOnMainThread();
        Log.d(TAG, "stopScoAudio: BT state=" + bluetoothState + ", " + "SCO is on: " + isSreplaced());
        if (bluetoothState != State.SCO_CONNECTING && bluetoothState != State.SCO_CONNECTED) {
            return;
        }
        cancelTimer();
        audioManager.stopBluetoothSco();
        audioManager.setBluetoothSreplaced(false);
        bluetoothState = State.SCO_DISCONNECTING;
        Log.d(TAG, "stopScoAudio done: BT state=" + bluetoothState + ", " + "SCO is on: " + isSreplaced());
    }

    /**
     * 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.
     */
    public void updateDevice() {
        if (bluetoothState == State.UNINITIALIZED || bluetoothHeadset == null) {
            return;
        }
        Log.d(TAG, "updateDevice");
        // Get connected devices for the headset profile. Returns the set of
        // devices which are in state STATE_CONNECTED. The BluetoothDevice clreplaced
        // is just a thin wrapper for a Bluetooth hardware address.
        List<BluetoothDevice> devices = bluetoothHeadset.getConnectedDevices();
        if (devices.isEmpty()) {
            bluetoothDevice = null;
            bluetoothState = State.HEADSET_UNAVAILABLE;
            Log.d(TAG, "No connected bluetooth headset");
        } else {
            // Always use first device in list. Android only supports one device.
            bluetoothDevice = devices.get(0);
            bluetoothState = State.HEADSET_AVAILABLE;
            Log.d(TAG, "Connected bluetooth headset: " + "name=" + bluetoothDevice.getName() + ", " + "state=" + stateToString(bluetoothHeadset.getConnectionState(bluetoothDevice)) + ", SCO audio=" + bluetoothHeadset.isAudioConnected(bluetoothDevice));
        }
        Log.d(TAG, "updateDevice done: BT state=" + bluetoothState);
    }

    /**
     * Stubs for test mocks.
     */
    @Nullable
    protected AudioManager getAudioManager(Context context) {
        return (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
    }

    protected void registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
        apprtcContext.registerReceiver(receiver, filter);
    }

    protected void unregisterReceiver(BroadcastReceiver receiver) {
        apprtcContext.unregisterReceiver(receiver);
    }

    protected boolean getBluetoothProfileProxy(Context context, BluetoothProfile.ServiceListener listener, int profile) {
        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(TAG, "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(TAG, "paired devices:");
            for (BluetoothDevice device : pairedDevices) {
                Log.d(TAG, " name=" + device.getName() + ", address=" + device.getAddress());
            }
        }
    }

    /**
     * Ensures that the audio manager updates its list of available audio devices.
     */
    private void updateAudioDeviceState() {
        ThreadUtils.checkIsOnMainThread();
        Log.d(TAG, "updateAudioDeviceState");
        apprtcAudioManager.updateAudioDeviceState();
    }

    /**
     * Starts timer which times out after BLUETOOTH_SCO_TIMEOUT_MS milliseconds.
     */
    private void startTimer() {
        ThreadUtils.checkIsOnMainThread();
        Log.d(TAG, "startTimer");
        handler.postDelayed(bluetoothTimeoutRunnable, BLUETOOTH_SCO_TIMEOUT_MS);
    }

    /**
     * Cancels any outstanding timer tasks.
     */
    private void cancelTimer() {
        ThreadUtils.checkIsOnMainThread();
        Log.d(TAG, "cancelTimer");
        handler.removeCallbacks(bluetoothTimeoutRunnable);
    }

    /**
     * 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() {
        ThreadUtils.checkIsOnMainThread();
        if (bluetoothState == State.UNINITIALIZED || bluetoothHeadset == null) {
            return;
        }
        Log.d(TAG, "bluetoothTimeout: BT state=" + bluetoothState + ", " + "attempts: " + scoConnectionAttempts + ", " + "SCO is on: " + isSreplaced());
        if (bluetoothState != State.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, "SCO connected with " + bluetoothDevice.getName());
                scoConnected = true;
            } else {
                Log.d(TAG, "SCO is not connected with " + bluetoothDevice.getName());
            }
        }
        if (scoConnected) {
            // We thought BT had timed out, but it's actually on; updating state.
            bluetoothState = State.SCO_CONNECTED;
            scoConnectionAttempts = 0;
        } else {
            // Give up and "cancel" our request by calling stopBluetoothSco().
            Log.w(TAG, "BT failed to connect after timeout");
            stopScoAudio();
        }
        updateAudioDeviceState();
        Log.d(TAG, "bluetoothTimeout done: BT state=" + bluetoothState);
    }

    /**
     * Checks whether audio uses Bluetooth SCO.
     */
    private boolean isSreplaced() {
        return audioManager.isBluetoothSreplaced();
    }

    /**
     * Converts BluetoothAdapter states into local string representations.
     */
    private String stateToString(int state) {
        switch(state) {
            case BluetoothAdapter.STATE_DISCONNECTED:
                return "DISCONNECTED";
            case BluetoothAdapter.STATE_CONNECTED:
                return "CONNECTED";
            case BluetoothAdapter.STATE_CONNECTING:
                return "CONNECTING";
            case BluetoothAdapter.STATE_DISCONNECTING:
                return "DISCONNECTING";
            case BluetoothAdapter.STATE_OFF:
                return "OFF";
            case BluetoothAdapter.STATE_ON:
                return "ON";
            case BluetoothAdapter.STATE_TURNING_OFF:
                // 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
                // for STATE_ON before attempting to use the adapter.
                return "TURNING_ON";
            default:
                return "INVALID";
        }
    }
}

16 Source : VoiceMediator.java
with Apache License 2.0
from LingjuAI

/**
 * Created by Administrator on 2016/11/5.
 */
public clreplaced VoiceMediator implements SystemVoiceMediator {

    private final static String TAG = "VoiceMediator";

    public final static int AUTO_TYPE = 0;

    public final static int XIMALAYA_TYPE = 1;

    private static VoiceMediator instance;

    // 识别引擎
    protected RecognizerBase recognizer;

    // 合成引擎
    protected SynthesizerBase synthesizer;

    // 唤醒引擎
    protected WakeupEngineBase awakener;

    private ChatStateListener chatListener;

    private Context mContext;

    private boolean isHeadSet;

    private BluetoothA2dp bluetoothA2dp;

    private BluetoothHeadset bluetoothHeadset;

    private boolean isBlueHeadSet;

    private boolean suportA2DP;

    private int remindDialogFlag;

    // 记录最新一次对话处于什么动作场景,用于判断不说话时是否推送NOANSWER
    private CmdAction currentAction;

    /**
     * 是否处于唤醒录音模式状态标记
     */
    private boolean is_wakeup_mode = false;

    /**
     * 音频播放类型(0:歌曲  1:有声内容)
     */
    private int audioPlayType = AUTO_TYPE;

    /**
     * 是否正在通话中
     */
    private boolean mobileRing = false;

    /**
     * 是否处于来电响铃中
     */
    private boolean calling = false;

    // 当前媒体音量
    private int mCurrentVolume;

    private AudioManager mAudioManager;

    private AtomicBoolean robotResponse = new AtomicBoolean(false);

    protected SoundPool soundPool = new SoundPool(1, AudioManager.STREAM_MUSIC, 100);

    protected int waitID;

    protected int waitPlayID;

    private boolean is_walk_navi;

    protected VoiceMediator(Context context) {
        this.mContext = context;
        initVoiceComponent();
        try {
            waitID = soundPool.load(mContext.getreplacedets().openFd(mContext.getResources().getString(R.string.audio_wait_response)), 1);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            EventBus.getDefault().register(this);
        }
    }

    public static synchronized VoiceMediator create(Context context) {
        if (instance == null)
            instance = new VoiceMediator(context);
        return instance;
    }

    public static VoiceMediator get() {
        return instance;
    }

    protected void initVoiceComponent() {
        Log.i(TAG, "initVoiceComponent>>>>>>>>>>>>>>>>>>>>>>>>>>>");
        /*tPools.execute(new Runnable() {
            @Override
			public void run() {
				for(String s:BaiduSynthesizer.OFFLINE_RS){
					copyFromreplacedetsToSdcard(RobotApplication.firstOpen,s,Setting.DEFAULT_DIR+"/"+s);
				}
				synthesizer = BaiduSynthesizer.createInstance(RobotService.this);

			}
		});*/
        // recognizer= BaiduRecognizer.createInstance(RobotService.this);
        SpeechUtility.createUtility(mContext, SpeechConstant.APPID + "=" + Constants.XUNFEI_APPID + "," + SpeechConstant.MODE_MSC + "=" + SpeechConstant.MODE_AUTO);
        // com.iflytek.cloud.Setting.setLogLevel(Setting.LOG_LEVEL.detail);
        com.iflytek.cloud.Setting.setShowLog(false);
        com.iflytek.cloud.Setting.setLocationEnable(true);
        Single.just(0).doOnSubscribe(new Consumer<Disposable>() {

            @Override
            public void accept(Disposable disposable) throws Exception {
                recognizer = IflyRecognizer.createInstance(mContext, VoiceMediator.this);
                synthesizer = IflySynthesizer.createInstance(mContext, VoiceMediator.this);
                awakener = VoiceAwakener.createInstance(mContext, VoiceMediator.this);
            }
        }).subscribeOn(Schedulers.newThread()).subscribe();
        Log.i(TAG, "initVoiceComponent>>>>>>>>>>>>>>>>>>>>>>>>>>>END");
    }

    /**
     * 耳机操作事件处理
     */
    @Subscribe
    public void onHeadSetEvent(HeadSetEvent e) {
        switch(e) {
            case Connect:
                this.isHeadSet = true;
                getHook();
                EventBus.getDefault().post(new ChatMsgEvent(new ResponseMsg(mContext.getResources().getString(R.string.headset_mode_tips)), null, null, null));
                break;
            case Disconnect:
                this.isHeadSet = false;
                if (mAudioManager != null)
                    mAudioManager.abandonAudioFocus(mFocusChangeListener);
                break;
            case Click:
                break;
            case DoubleClick:
                break;
            case TrebleClick:
                break;
        }
    }

    /**
     * 获取音频焦点,夺回耳机按键广播
     */
    public void getHook() {
        if (AppConfig.dPreferences.getBoolean(AppConfig.ALLOW_WIRE, true)) {
            if (mAudioManager == null)
                mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
            // 抢回焦点
            /**
             * 由于耳机广播注册对象是单例且广播接受者唯一的,如果接收者栈中已存在该接收者对象则直接返回。
             * 所以在重新注册本应用的耳机广播接收者之前需要先注销已失效但仍存在的接收者 *
             */
            mAudioManager.unregisterMediaButtonEventReceiver(new ComponentName(mContext.getApplicationContext(), MediaButtonReceiver.clreplaced));
            isFocus = mAudioManager.requestAudioFocus(mFocusChangeListener, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN) != AudioManager.AUDIOFOCUS_REQUEST_FAILED;
            /* 注册耳机广播接收者 */
            mAudioManager.registerMediaButtonEventReceiver(new ComponentName(mContext.getApplicationContext(), MediaButtonReceiver.clreplaced));
            Log.i("LingJu", "MainActivity getHook():" + mAudioManager.isMusicActive());
        }
    }

    public boolean isFocus() {
        return isFocus;
    }

    /**
     * 是否获取到音频焦点标记
     */
    private boolean isFocus = true;

    private AudioManager.OnAudioFocusChangeListener mFocusChangeListener = new AudioManager.OnAudioFocusChangeListener() {

        @Override
        public void onAudioFocusChange(int focusChange) {
            Log.i("LingJu", "焦点状态:" + focusChange);
            switch(focusChange) {
                case AudioManager.AUDIOFOCUS_LOSS:
                case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
                case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
                    isFocus = false;
                    mAudioManager.abandonAudioFocus(mFocusChangeListener);
                    break;
                case AudioManager.AUDIOFOCUS_GAIN:
                    Log.i("LingJu", "MainActivity onAudioFocusChange()");
                    isFocus = true;
                    break;
            }
        }
    };

    public void setBluetoothHeadset(BluetoothHeadset bluetoothHeadset) {
        this.bluetoothHeadset = bluetoothHeadset;
    }

    public void setBluetoothA2dp(BluetoothA2dp bluetoothA2dp) {
        this.bluetoothA2dp = bluetoothA2dp;
    }

    @Override
    public BluetoothHeadset getBluetoothHeadset() {
        return bluetoothHeadset;
    }

    @Override
    public BluetoothA2dp getBluetoothA2dp() {
        return bluetoothA2dp;
    }

    public void setBlueHeadSet(boolean blueHeadSet) {
        isBlueHeadSet = blueHeadSet;
    }

    public void setSuportA2DP(boolean suportA2DP) {
        this.suportA2DP = suportA2DP;
    }

    /**
     * 设置合成参数
     */
    public void setSynthParams(NetUtil.NetType type) {
        synthesizer.resetParam(type);
    }

    @Override
    public boolean isPlaying() {
        return LingjuAudioPlayer.get().isPlaying();
    }

    @Override
    public boolean isTinging() {
        return XmPlayerManager.getInstance(mContext).isPlaying();
    }

    @Override
    public void setAudioPlayType(int playType) {
        this.audioPlayType = playType;
    }

    @Override
    public int getAudioPlayType() {
        return audioPlayType;
    }

    @Override
    public boolean isHeadset() {
        return isHeadSet;
    }

    public void setHeadSet(boolean isHeadSet) {
        this.isHeadSet = isHeadSet;
    }

    @Override
    public boolean isWakeUpMode() {
        return is_wakeup_mode;
    }

    @Override
    public void setWakeupModeFlag(boolean isWakeUpMode) {
        is_wakeup_mode = isWakeUpMode;
    }

    @Override
    public boolean isBlueToothHeadSet() {
        return isBlueHeadSet;
    }

    public boolean isSuportA2DP() {
        return suportA2DP;
    }

    @Override
    public void openBlueHeadSet4Recognition() {
        if (isBlueToothHeadSet() && getBluetoothHeadset() != null && !getBluetoothHeadset().getConnectedDevices().isEmpty()) {
            int c = 0;
            BluetoothDevice device = getBluetoothHeadset().getConnectedDevices().get(0);
            while (isBlueToothHeadSet() && !getBluetoothHeadset().isAudioConnected(device)) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                Log.i(TAG, "c=" + c);
                c++;
                if (c > 30) {
                    break;
                }
            }
        }
    }

    @Override
    public void startBluetoothSco() {
        if (isBlueToothHeadSet()) {
            BluetoothHeadset headset = getBluetoothHeadset();
            if (headset == null || headset.getConnectedDevices().isEmpty() || headset.isAudioConnected(headset.getConnectedDevices().get(0)))
                return;
            Log.e(TAG, "startBluetoothSco:device size=" + headset.getConnectedDevices().size());
            headset.startVoiceRecognition(headset.getConnectedDevices().get(0));
        }
    }

    @Override
    public void stopBluetoothSco() {
        if (isBlueToothHeadSet() && getBluetoothHeadset() != null && !getBluetoothHeadset().getConnectedDevices().isEmpty()) {
            if (getBluetoothHeadset().isAudioConnected(getBluetoothHeadset().getConnectedDevices().get(0))) {
                Log.e(TAG, "stopBluetoothSco>>stopBluetoothSco");
                getBluetoothHeadset().stopVoiceRecognition(getBluetoothHeadset().getConnectedDevices().get(0));
                try {
                    int c = 0;
                    while (getBluetoothHeadset().isAudioConnected(getBluetoothHeadset().getConnectedDevices().get(0))) {
                        Thread.sleep(100);
                        Log.e(TAG, "stopBluetoothSco c=" + c);
                        c++;
                        if (c > 10) {
                            break;
                        }
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }

    @Override
    public void stopSynthesize() {
        if (synthesizer != null) {
            Log.i(TAG, "stopsynthe................");
            synthesizer.stopSpeakingAbsolte();
        }
    }

    /**
     * 停止识别和说话并且尝试打开唤醒(如果当前为唤醒模式则可以打开)
     */
    public void stopSpeakAndWakeup(boolean wakeup) {
        stopRecognize();
        synthesizer.stopSpeakingAbsolte();
        if (wakeup)
            tryToWakeup();
    }

    /**
     * 尝试打开唤醒(如果当前为唤醒模式则可以打开)
     */
    @Override
    public void tryToWakeup() {
        Log.i("LingJu", "VoiceMediator tryToWakeup()>>> " + is_wakeup_mode);
        if (is_wakeup_mode)
            startWakeup();
    }

    @Override
    public void changeMediaVolume(int percent) {
        AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
        mCurrentVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
        int maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
        int volume = (int) ((percent / 100.0) * maxVolume + 0.5);
        audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, volume, 0);
        audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC, AudioManager.ADJUST_SAME, AudioManager.FLAG_PLAY_SOUND | AudioManager.FLAG_SHOW_UI);
    }

    @Override
    public void recordCurrentVolume() {
        is_walk_navi = true;
        AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
        mCurrentVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
    }

    @Override
    public void resumeMediaVolume() {
        is_walk_navi = false;
        AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
        audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, mCurrentVolume, 0);
    }

    @Override
    public boolean isWalkNavi() {
        return is_walk_navi;
    }

    @Override
    public void stopRecognize() {
        Log.i(TAG, "stopRecognize");
        if (recognizer != null) {
            recognizer.stopRecognize();
        }
    }

    @Override
    public void onWakenup(String wakeupWord) {
        /* 唤醒成功,开启识别 */
        EventBus.getDefault().post(new RecordUpdateEvent(RecordUpdateEvent.RECORDING));
        startRecognize();
    }

    @Override
    public void sendMsg2Robot(String msg) {
        if (chatListener != null)
            chatListener.onInput(msg);
    }

    @Override
    public void setWakeUpMode(boolean flag) {
        if (synthesizer != null) {
            is_wakeup_mode = flag;
            AppConfig.dPreferences.edit().putBoolean(AppConfig.WAKEUP_MODE, flag).commit();
            if (flag) {
                if (synthesizer.isSpeaking() || recognizer.isRecognizing()) {
                    return;
                } else {
                    startWakeup();
                }
            } else {
                stopWakenup();
            }
        }
    }

    @Override
    public boolean allowSynthersize(SpeechMsg msg) {
        boolean result = true;
        try {
            if (BNavigatorProxy.getInstance().isNaviBegin() && msg.origin() == SpeechMsg.ORIGIN_DEFAULT) {
                msg.getBuilder().setOrigin(SpeechMsg.ORIGIN_COMMON);
            }
            /*if (!isHeadSet) {     //合成声音先关闭唤醒
                stopWakenup();
            }*/
            if (msg.priority() == SpeechMsg.PRIORITY_ABOVE_RECOGNIZE) {
                // 合成优先
                Intent intent = new Intent(mContext, replacedistantService.clreplaced);
                intent.putExtra(replacedistantService.CMD, replacedistantService.ServiceCmd.STOP_RECOGNIZE);
                mContext.startService(intent);
                EventBus.getDefault().post(new RecordUpdateEvent(RecordUpdateEvent.RECORD_IDLE));
            } else {
                if (recognizer.isRecognizing()) {
                    // 识别优先
                    result = false;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        Log.i(TAG, "allowSynthersize>>" + Boolean.toString(result));
        return result;
    }

    @Override
    public boolean compareSpeechMsg(SpeechMsg nSpeechMsg, SpeechMsg currentSpeechMsg) {
        if (currentSpeechMsg != null && currentSpeechMsg.origin() == SpeechMsg.ORIGIN_NAVI) {
            if (nSpeechMsg.origin() != SpeechMsg.ORIGIN_NAVI) {
                if (nSpeechMsg.origin() == SpeechMsg.ORIGIN_DEFAULT)
                    nSpeechMsg.getBuilder().setOrigin(SpeechMsg.ORIGIN_COMMON);
                return false;
            }
        }
        return true;
    }

    public void setCurrentAction(CmdAction action) {
        this.currentAction = action;
    }

    @Override
    public void onRecognizeError(int code, String msg) {
        EventBus.getDefault().post(new RecordUpdateEvent(RecordUpdateEvent.RECORD_IDLE_AFTER_RECOGNIZED));
        if (recognizer.isPlayingBeforRecognize()) {
            EventBus.getDefault().post(LingjuAudioPlayer.get().currentPlayMusic());
            LingjuAudioPlayer.get().play();
        }
        if (recognizer.isTingBeforeRecognize()) {
            XmPlayerManager.getInstance(mContext).play();
        }
        tryToWakeup();
        boolean showError = false;
        switch(code) {
            // 10114
            case ErrorCode.MSP_ERROR_TIME_OUT:
            // 20002
            case ErrorCode.ERROR_NETWORK_TIMEOUT:
            // 20003
            case ErrorCode.ERROR_NET_EXCEPTION:
            case // 20001
            ErrorCode.ERROR_NO_NETWORK:
                msg = Setting.RECOGNIZE_NETWORK_ERROR;
                break;
            case 20005:
                msg = Setting.RECOGNIZE_NOMATCH_ERROR;
                break;
            case // 10118
            ErrorCode.MSP_ERROR_NO_DATA:
                // TODO: 2017/5/18 等待拨号、发短信时不做任何回复
                if ((currentAction == null || ((currentAction.getOutc() & DefaultProcessor.OUTC_ASK) != DefaultProcessor.OUTC_ASK)) && !EventBus.getDefault().hreplacedubscriberForEvent(NavigateEvent.clreplaced)) {
                    msg = Setting.RECOGNIZE_NODATA_ERROR;
                } else {
                    Intent intent = new Intent(mContext, replacedistantService.clreplaced);
                    intent.putExtra(replacedistantService.CMD, replacedistantService.ServiceCmd.SEND_TO_ROBOT);
                    intent.putExtra(replacedistantService.TEXT, ChatRobotBuilder.NOANSWER);
                    mContext.startService(intent);
                    return;
                }
                break;
            case 20017:
                msg = "很抱歉,识别引擎出错,请关闭程序重试!";
                showError = true;
                break;
            case ErrorCode.ERROR_AUDIO_RECORD:
                msg = msg + ",请您在设置-应用管理-应用详情下的权限管理检查是否允许录音权限";
                showError = true;
                break;
            default:
                msg = Setting.RECOGNIZE_ERROR_TIPS;
                break;
        }
        if (showError) {
            EventBus.getDefault().post(new SynthesizeEvent());
        }
        if (!TextUtils.isEmpty(msg)) /* && code == ErrorCode.MSP_ERROR_NO_DATA*/
        {
            EventBus.getDefault().post(new ChatMsgEvent(new ResponseMsg(msg), null, null, null));
            speak(new SpeechMsgBuilder(msg), true);
        }
    }

    private void speak(SpeechMsgBuilder msgBuilder, final boolean isAnim) {
        SynthesizerBase.get().startSpeakAbsolute(msgBuilder.build()).doOnNext(new Consumer<SpeechMsg>() {

            @Override
            public void accept(SpeechMsg speechMsg) throws Exception {
                if (speechMsg.state() == SpeechMsg.State.OnBegin && isAnim)
                    EventBus.getDefault().post(new SynthesizeEvent(SynthesizeEvent.SYNTH_START));
            }
        }).doOnComplete(new Action() {

            @Override
            public void run() throws Exception {
                EventBus.getDefault().post(new SynthesizeEvent(SynthesizeEvent.SYNTH_END));
            }
        }).subscribeOn(Schedulers.io()).observeOn(Schedulers.computation()).subscribe();
    }

    @Override
    public void onRecoginzeWait() {
        EventBus.getDefault().post(new RecordUpdateEvent(RecordUpdateEvent.RECOGNIZING));
    }

    @Override
    public void onRecoginzeResult(String result) {
        Log.i(TAG, "onRecoginzeResult>>" + result);
        if (EventBus.getDefault().hreplacedubscriberForEvent(NavigateEvent.clreplaced)) {
            EventBus.getDefault().post(new NavigateEvent(NavigateEvent.STOP_COUNTDOWN));
        }
        if (recognizer.isPlayingBeforRecognize()) {
            PlayMusic music = LingjuAudioPlayer.get().currentPlayMusic();
            if (music != null)
                EventBus.getDefault().post(music);
            LingjuAudioPlayer.get().play();
        }
        if (recognizer.isTingBeforeRecognize()) {
            XmPlayerManager.getInstance(mContext).play();
        }
        EventBus.getDefault().post(new RecordUpdateEvent(RecordUpdateEvent.RECORD_IDLE_AFTER_RECOGNIZED));
        if (!TextUtils.isEmpty(result) && !"。".equals(result)) {
            // 将识别结果发送给机器人
            if (chatListener != null) {
                robotResponse.set(false);
                Observable.just(0).delay(750, TimeUnit.MILLISECONDS).filter(new Predicate<Integer>() {

                    @Override
                    public boolean test(Integer integer) throws Exception {
                        Log.i("LingJu", "过滤器:" + (!robotResponse.get()));
                        return !robotResponse.get();
                    }
                }).doOnNext(new Consumer<Integer>() {

                    @Override
                    public void accept(Integer integer) throws Exception {
                        if (!robotResponse.get()) {
                            Log.i("LingJu", "播放等待提示音");
                            AudioManager mgr = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
                            float streamVolumeCurrent = mgr.getStreamVolume(AudioManager.STREAM_MUSIC);
                            float streamVolumeMax = mgr.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
                            float volume = streamVolumeCurrent / streamVolumeMax;
                            waitPlayID = soundPool.play(waitID, volume, volume, 1, -1, 1f);
                        }
                    }
                }).observeOn(Schedulers.io()).subscribeOn(Schedulers.io()).subscribe();
                chatListener.onInput(result);
                /* 添加模式不需要将识别文本添加聊天视图 */
                if (result.contains("&"))
                    result = result.substring(0, result.lastIndexOf("&"));
                EventBus.getDefault().post(new ChatMsgEvent(new com.lingju.model.temp.speech.SpeechMsg(result), null, null, null));
            }
        } else {
            onRecognizeError(ErrorCode.MSP_ERROR_NO_DATA, "");
        }
    }

    /**
     * 结束录音模式时调用
     */
    @Override
    public void onTapeResult(final String tapeContent) {
        if (TextUtils.isEmpty(tapeContent)) {
            currentAction.setOutc(1);
            onRecognizeError(ErrorCode.MSP_ERROR_NO_DATA, Setting.RECOGNIZE_NODATA_ERROR);
            return;
        }
        // 退出录音模式
        IflyRecognizer.getInstance().setRecognizeMode(false);
        String tapePath = PcmRecorder.TAPE_SRC + System.currentTimeMillis() + ".amr";
        // 压缩并保存录音文件
        AmrEncoder.pcm2Amr(PcmRecorder.TEMP_FILE, tapePath);
        final Tape tape = new Tape();
        tape.setText(tapeContent);
        tape.setUrl(tapePath);
        tape.setCreated(new Date());
        tape.setSynced(false);
        // 保存录音记录
        TapeEnreplacedyDao.getInstance().insertTape(tape);
        // 上传录音对象方式1(新策略,暂未实现)
        // uploadTapeEnreplacedy(tape);
        // Log.i("LingJu", "onSyncComplete()>>>" + tape.getSid());
        TapeEnreplacedyDao.getInstance().setOnSyncListener(new TapeEnreplacedyDao.OnSyncListener() {

            @Override
            public void onSyncComplete() {
                Log.i("LingJu", "onSyncComplete()>>>" + tape.getSid());
                onRecoginzeResult(tapeContent + "&" + tape.getSid());
            }
        });
        // //上传录音对象方式2(同步记录)
        TapeEnreplacedyDao.getInstance().sync();
    }

    private void uploadTapeEnreplacedy(Tape tape) {
        TapeEnreplacedy enreplacedy = new TapeEnreplacedy();
        enreplacedy.setText(tape.getText());
        enreplacedy.setUrl(tape.getUrl());
        enreplacedy.setCreated(tape.getCreated());
        enreplacedy.setModified(tape.getModified());
        enreplacedy.setLid(tape.getId().intValue());
        enreplacedy.setSid(tape.getSid());
        enreplacedy.setRecyle(tape.getRecyle());
        enreplacedy.setSynced(tape.getSynced());
        enreplacedy.setTimestamp(tape.getTimestamp());
        AndroidChatRobotBuilder.get().robot().append(enreplacedy);
    }

    @Override
    public void stopWaitPlay() {
        soundPool.stop(waitPlayID);
    }

    @Override
    public void setRobotResponse(boolean hasResponse) {
        robotResponse.set(hasResponse);
    }

    /**
     * 长时间录音识别结果回调,该模式下不需要将识别文本作为对话输出
     */
    @Override
    public void onLongRecoginzeResult(String result) {
        EventBus.getDefault().post(new LongRecognizeEvent(result));
    }

    @Override
    public void startRecognize() {
        Log.i(TAG, "startRecognize");
        if (synthesizer.isSpeaking()) {
            synthesizer.stopSpeakingAbsolte();
        }
        recognizer.startRecognize();
    }

    /**
     * 提醒提前天数弹窗设置标记
     */
    public void setRemindDialogFlag(int flag) {
        // 0代表默认、1代表提醒天数弹窗
        remindDialogFlag = flag;
    }

    /**
     * 根据msg设置的模式进行对应的语音操作
     */
    @Override
    public void keepVoiceCtrl(SpeechMsg msg) {
        switch(msg.contextMode()) {
            case SpeechMsg.CONTEXT_KEEP_RECOGNIZE:
                if (msg.state() == SpeechMsg.State.Completed) {
                    startRecognize();
                    // remindDialogFlag为0代表默认、1代表提醒天数弹窗
                    EventBus.getDefault().post(new RecordUpdateEvent(RecordUpdateEvent.RECORDING, remindDialogFlag));
                }
                break;
            case SpeechMsg.CONTEXT_KEEP_AWAKEN:
                tryToWakeup();
                break;
            default:
                break;
        }
    }

    @Override
    public void setCalling(boolean isCalling) {
        this.calling = isCalling;
    }

    @Override
    public boolean isCalling() {
        return calling;
    }

    @Override
    public void setMobileRing(boolean mobileRing) {
        this.mobileRing = mobileRing;
    }

    @Override
    public boolean mobileRing() {
        return mobileRing;
    }

    @Override
    public void pausePlay() {
        LingjuAudioPlayer.get().pause();
    }

    @Override
    public void pauseTing() {
        XmPlayerManager.getInstance(mContext).pause();
    }

    @Override
    public void startWakeup() {
        if (awakener != null) {
            awakener.startListening();
        }
    }

    /**
     * 关闭唤醒
     */
    @Override
    public void stopWakenup() {
        if (awakener != null)
            awakener.stopCompletely();
    }

    @Override
    public void updateLexicon() {
        String[] keywords = mContext.getResources().getStringArray(R.array.keywords);
        UserWords userWords = new UserWords();
        for (String keyword : keywords) {
            userWords.putWord(keyword);
        }
        recognizer.updateLexicon(userWords.toString());
    }

    @Override
    public void onRecoginzeBegin() {
        Log.i(TAG, "onRecoginzeBegin");
        stopWaitPlay();
    }

    @Override
    public void onRecoginzeVolumeChanged(int v) {
        EventBus.getDefault().post(new VolumeChangedEvent(v));
    }

    @Override
    public boolean preToCall() {
        return false;
    }

    @Override
    public void onSynthesizerInited(int code) {
    }

    @Override
    public void onSynthesizerError(String errorMsg) {
        EventBus.getDefault().post(new ChatMsgEvent(new ResponseMsg(errorMsg), null, null, null));
    }

    // 打开扬声器
    @Override
    public boolean openSpeaker() {
        Log.e(TAG, "openSpeaker");
        if (isHeadSet)
            return false;
        try {
            mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
            switch(mAudioManager.getMode()) {
                case AudioManager.MODE_IN_CALL:
                    System.out.println("MODE_IN_CALL");
                    break;
                case AudioManager.MODE_IN_COMMUNICATION:
                    System.out.println("MODE_IN_COMMUNICATION");
                    break;
                case AudioManager.MODE_NORMAL:
                    System.out.println("MODE_NORMAL");
                    break;
                case AudioManager.MODE_RINGTONE:
                    System.out.println("MODE_RINGTONE");
                    break;
                case AudioManager.MODE_INVALID:
                    System.out.println("MODE_INVALID");
                    break;
            }
            if (!mAudioManager.isSpeakerphoneOn() && mAudioManager.getMode() == AudioManager.MODE_IN_CALL) {
                Log.e(TAG, "openSpeaker>>setSpeakerphoneOn");
                mAudioManager.setSpeakerphoneOn(true);
                mAudioManager.setStreamVolume(AudioManager.STREAM_VOICE_CALL, mAudioManager.getStreamMaxVolume(AudioManager.STREAM_VOICE_CALL), AudioManager.STREAM_VOICE_CALL);
                return true;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }

    @Override
    public Handler createHandler() {
        return new Handler(Looper.getMainLooper());
    }

    public void setChatStateListener(ChatStateListener chatListener) {
        this.chatListener = chatListener;
    }
}

16 Source : AppRTCBluetoothManager.java
with ISC License
from kangshaojun

/**
 * AppRTCProximitySensor manages functions related to Bluetoth devices in the
 * AppRTC demo.
 */
public clreplaced AppRTCBluetoothManager {

    private static final String TAG = "AppRTCBluetoothManager";

    // 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;

    // Bluetooth connection state.
    public enum State {

        // Bluetooth is not available; no adapter or Bluetooth is off.
        UNINITIALIZED,
        // Bluetooth error happened when trying to start Bluetooth.
        ERROR,
        // Bluetooth proxy object for the Headset profile exists, but no connected headset devices,
        // SCO is not started or disconnected.
        HEADSET_UNAVAILABLE,
        // Bluetooth proxy object for the Headset profile 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
    }

    private final Context apprtcContext;

    private final FlutterIncallManagerPlugin apprtcAudioManager;

    private final AudioManager audioManager;

    private final Handler handler;

    int scoConnectionAttempts;

    private State bluetoothState;

    private final BluetoothProfile.ServiceListener bluetoothServiceListener;

    private BluetoothAdapter bluetoothAdapter;

    private BluetoothHeadset bluetoothHeadset;

    private BluetoothDevice bluetoothDevice;

    private final BroadcastReceiver bluetoothHeadsetReceiver;

    // 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();
        }
    };

    /**
     * Implementation of an interface that notifies BluetoothProfile IPC clients when they have been
     * connected to or disconnected from the service.
     */
    private clreplaced BluetoothServiceListener implements BluetoothProfile.ServiceListener {

        @Override
        public // connection and perform other operations that are relevant to the headset profile.
        void onServiceConnected(int profile, BluetoothProfile proxy) {
            if (profile != BluetoothProfile.HEADSET || bluetoothState == State.UNINITIALIZED) {
                return;
            }
            Log.d(TAG, "BluetoothServiceListener.onServiceConnected: BT state=" + bluetoothState);
            // Android only supports one connected Bluetooth Headset at a time.
            bluetoothHeadset = (BluetoothHeadset) proxy;
            updateAudioDeviceState();
            Log.d(TAG, "onServiceConnected done: BT state=" + bluetoothState);
        }

        @Override
        public /**
         * Notifies the client when the proxy object has been disconnected from the service.
         */
        void onServiceDisconnected(int profile) {
            if (profile != BluetoothProfile.HEADSET || bluetoothState == State.UNINITIALIZED) {
                return;
            }
            Log.d(TAG, "BluetoothServiceListener.onServiceDisconnected: BT state=" + bluetoothState);
            stopScoAudio();
            bluetoothHeadset = null;
            bluetoothDevice = null;
            bluetoothState = State.HEADSET_UNAVAILABLE;
            updateAudioDeviceState();
            Log.d(TAG, "onServiceDisconnected done: BT state=" + bluetoothState);
        }
    }

    // Intent broadcast receiver which handles changes in Bluetooth device availability.
    // Detects headset changes and Bluetooth SCO state changes.
    private clreplaced BluetoothHeadsetBroadcastReceiver extends BroadcastReceiver {

        @Override
        public void onReceive(Context context, Intent intent) {
            if (bluetoothState == State.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. Typically received when user turns on a BT
            // 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(TAG, "BluetoothHeadsetBroadcastReceiver.onReceive: " + "a=ACTION_CONNECTION_STATE_CHANGED, " + "s=" + stateToString(state) + ", " + "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.
                    stopScoAudio();
                    updateAudioDeviceState();
                }
            // 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(TAG, "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) {
                        Log.d(TAG, "+++ Bluetooth audio SCO is now connected");
                        bluetoothState = State.SCO_CONNECTED;
                        scoConnectionAttempts = 0;
                        updateAudioDeviceState();
                    } else {
                        Log.w(TAG, "Unexpected state BluetoothHeadset.STATE_AUDIO_CONNECTED");
                    }
                } else if (state == BluetoothHeadset.STATE_AUDIO_CONNECTING) {
                    Log.d(TAG, "+++ Bluetooth audio SCO is now connecting...");
                } else if (state == BluetoothHeadset.STATE_AUDIO_DISCONNECTED) {
                    Log.d(TAG, "+++ Bluetooth audio SCO is now disconnected");
                    if (isInitialStickyBroadcast()) {
                        Log.d(TAG, "Ignore STATE_AUDIO_DISCONNECTED initial sticky broadcast.");
                        return;
                    }
                    updateAudioDeviceState();
                }
            }
            Log.d(TAG, "onReceive done: BT state=" + bluetoothState);
        }
    }

    /**
     * Construction.
     */
    public static AppRTCBluetoothManager create(Context context, FlutterIncallManagerPlugin audioManager) {
        Log.d(TAG, "create");
        return new AppRTCBluetoothManager(context, audioManager);
    }

    protected AppRTCBluetoothManager(Context context, FlutterIncallManagerPlugin audioManager) {
        Log.d(TAG, "ctor");
        apprtcContext = context;
        apprtcAudioManager = audioManager;
        this.audioManager = getAudioManager(context);
        bluetoothState = State.UNINITIALIZED;
        bluetoothServiceListener = new BluetoothServiceListener();
        bluetoothHeadsetReceiver = new BluetoothHeadsetBroadcastReceiver();
        handler = new Handler(Looper.getMainLooper());
    }

    /**
     * Returns the internal state.
     */
    public State getState() {
        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 IncallPlugin is also involved in driving this state
     * change.
     */
    public void start() {
        Log.d(TAG, "start");
        if (!hasPermission(apprtcContext, android.Manifest.permission.BLUETOOTH)) {
            Log.w(TAG, "Process (pid=" + Process.myPid() + ") lacks BLUETOOTH permission");
            return;
        }
        if (bluetoothState != State.UNINITIALIZED) {
            Log.w(TAG, "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, "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, "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(apprtcContext, bluetoothServiceListener, BluetoothProfile.HEADSET)) {
            Log.e(TAG, "BluetoothAdapter.getProfileProxy(HEADSET) failed");
            return;
        }
        // Register receivers for BluetoothHeadset change notifications.
        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, "HEADSET profile state: " + stateToString(bluetoothAdapter.getProfileConnectionState(BluetoothProfile.HEADSET)));
        Log.d(TAG, "Bluetooth proxy for headset profile has started");
        bluetoothState = State.HEADSET_UNAVAILABLE;
        Log.d(TAG, "start done: BT state=" + bluetoothState);
    }

    /**
     * Stops and closes all components related to Bluetooth audio.
     */
    public void stop() {
        Log.d(TAG, "stop: BT state=" + bluetoothState);
        if (bluetoothAdapter == null) {
            return;
        }
        // Stop BT SCO connection with remote device if needed.
        stopScoAudio();
        // Close down remaining BT resources.
        if (bluetoothState == State.UNINITIALIZED) {
            return;
        }
        unregisterReceiver(bluetoothHeadsetReceiver);
        cancelTimer();
        if (bluetoothHeadset != null) {
            bluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, bluetoothHeadset);
            bluetoothHeadset = null;
        }
        bluetoothAdapter = null;
        bluetoothDevice = null;
        bluetoothState = State.UNINITIALIZED;
        Log.d(TAG, "stop done: BT state=" + bluetoothState);
    }

    /**
     * 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() {
        Log.d(TAG, "startSco: BT state=" + bluetoothState + ", " + "attempts: " + scoConnectionAttempts + ", " + "SCO is on: " + isSreplaced());
        if (scoConnectionAttempts >= MAX_SCO_CONNECTION_ATTEMPTS) {
            Log.e(TAG, "BT SCO connection fails - no more attempts");
            return false;
        }
        if (bluetoothState != State.HEADSET_AVAILABLE) {
            Log.e(TAG, "BT SCO connection fails - no headset available");
            return false;
        }
        // Start BT SCO channel and wait for ACTION_AUDIO_STATE_CHANGED.
        Log.d(TAG, "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.
        bluetoothState = State.SCO_CONNECTING;
        audioManager.startBluetoothSco();
        audioManager.setBluetoothSreplaced(true);
        scoConnectionAttempts++;
        startTimer();
        Log.d(TAG, "startScoAudio done: BT state=" + bluetoothState + ", " + "SCO is on: " + isSreplaced());
        return true;
    }

    /**
     * Stops Bluetooth SCO connection with remote device.
     */
    public void stopScoAudio() {
        Log.d(TAG, "stopScoAudio: BT state=" + bluetoothState + ", " + "SCO is on: " + isSreplaced());
        if (bluetoothState != State.SCO_CONNECTING && bluetoothState != State.SCO_CONNECTED) {
            return;
        }
        cancelTimer();
        audioManager.stopBluetoothSco();
        audioManager.setBluetoothSreplaced(false);
        bluetoothState = State.SCO_DISCONNECTING;
        Log.d(TAG, "stopScoAudio done: BT state=" + bluetoothState + ", " + "SCO is on: " + isSreplaced());
    }

    /**
     * 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.
     */
    public void updateDevice() {
        if (bluetoothState == State.UNINITIALIZED || bluetoothHeadset == null) {
            return;
        }
        Log.d(TAG, "updateDevice");
        // Get connected devices for the headset profile. Returns the set of
        // devices which are in state STATE_CONNECTED. The BluetoothDevice clreplaced
        // is just a thin wrapper for a Bluetooth hardware address.
        List<BluetoothDevice> devices = bluetoothHeadset.getConnectedDevices();
        if (devices.isEmpty()) {
            bluetoothDevice = null;
            bluetoothState = State.HEADSET_UNAVAILABLE;
            Log.d(TAG, "No connected bluetooth headset");
        } else {
            // Always use first device in list. Android only supports one device.
            bluetoothDevice = devices.get(0);
            bluetoothState = State.HEADSET_AVAILABLE;
            Log.d(TAG, "Connected bluetooth headset: " + "name=" + bluetoothDevice.getName() + ", " + "state=" + stateToString(bluetoothHeadset.getConnectionState(bluetoothDevice)) + ", SCO audio=" + bluetoothHeadset.isAudioConnected(bluetoothDevice));
        }
        Log.d(TAG, "updateDevice done: BT state=" + bluetoothState);
    }

    /**
     * Stubs for test mocks.
     */
    protected AudioManager getAudioManager(Context context) {
        return (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
    }

    protected void registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
        apprtcContext.registerReceiver(receiver, filter);
    }

    protected void unregisterReceiver(BroadcastReceiver receiver) {
        apprtcContext.unregisterReceiver(receiver);
    }

    protected boolean getBluetoothProfileProxy(Context context, BluetoothProfile.ServiceListener listener, int profile) {
        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(TAG, "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(TAG, "paired devices:");
    // for (BluetoothDevice device : pairedDevices) {
    // Log.d(TAG, " name=" + device.getName() + ", address=" + device.getAddress());
    // }
    // }
    }

    /**
     * Ensures that the audio manager updates its list of available audio devices.
     */
    private void updateAudioDeviceState() {
        Log.d(TAG, "updateAudioDeviceState");
        apprtcAudioManager.updateAudioDeviceState();
    }

    /**
     * Starts timer which times out after BLUETOOTH_SCO_TIMEOUT_MS milliseconds.
     */
    private void startTimer() {
        Log.d(TAG, "startTimer");
        handler.postDelayed(bluetoothTimeoutRunnable, BLUETOOTH_SCO_TIMEOUT_MS);
    }

    /**
     * Cancels any outstanding timer tasks.
     */
    private void cancelTimer() {
        Log.d(TAG, "cancelTimer");
        handler.removeCallbacks(bluetoothTimeoutRunnable);
    }

    /**
     * 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 (bluetoothState == State.UNINITIALIZED || bluetoothHeadset == null) {
            return;
        }
        Log.d(TAG, "bluetoothTimeout: BT state=" + bluetoothState + ", " + "attempts: " + scoConnectionAttempts + ", " + "SCO is on: " + isSreplaced());
        if (bluetoothState != State.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, "SCO connected with " + bluetoothDevice.getName());
                scoConnected = true;
            } else {
                Log.d(TAG, "SCO is not connected with " + bluetoothDevice.getName());
            }
        }
        if (scoConnected) {
            // We thought BT had timed out, but it's actually on; updating state.
            bluetoothState = State.SCO_CONNECTED;
            scoConnectionAttempts = 0;
        } else {
            // Give up and "cancel" our request by calling stopBluetoothSco().
            Log.w(TAG, "BT failed to connect after timeout");
            stopScoAudio();
        }
        updateAudioDeviceState();
        Log.d(TAG, "bluetoothTimeout done: BT state=" + bluetoothState);
    }

    /**
     * Checks whether audio uses Bluetooth SCO.
     */
    private boolean isSreplaced() {
        return audioManager.isBluetoothSreplaced();
    }

    /**
     * Converts BluetoothAdapter states into local string representations.
     */
    private String stateToString(int state) {
        switch(state) {
            case BluetoothAdapter.STATE_DISCONNECTED:
                return "DISCONNECTED";
            case BluetoothAdapter.STATE_CONNECTED:
                return "CONNECTED";
            case BluetoothAdapter.STATE_CONNECTING:
                return "CONNECTING";
            case BluetoothAdapter.STATE_DISCONNECTING:
                return "DISCONNECTING";
            case BluetoothAdapter.STATE_OFF:
                return "OFF";
            case BluetoothAdapter.STATE_ON:
                return "ON";
            case BluetoothAdapter.STATE_TURNING_OFF:
                // 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
                // for STATE_ON before attempting to use the adapter.
                return "TURNING_ON";
            default:
                return "INVALID";
        }
    }
}

15 Source : VoiceMediator.java
with Apache License 2.0
from LingjuAI

@Override
public void startBluetoothSco() {
    if (isBlueToothHeadSet()) {
        BluetoothHeadset headset = getBluetoothHeadset();
        if (headset == null || headset.getConnectedDevices().isEmpty() || headset.isAudioConnected(headset.getConnectedDevices().get(0)))
            return;
        Log.e(TAG, "startBluetoothSco:device size=" + headset.getConnectedDevices().size());
        headset.startVoiceRecognition(headset.getConnectedDevices().get(0));
    }
}

15 Source : BluetoothManagerTest.java
with MIT License
from lgyjg

/**
 * Verifies basic behavior of the AppRTCBluetoothManager clreplaced.
 * Note that the test object uses an AppRTCAudioManager (injected in ctor),
 * but a mocked version is used instead. Hence, the parts "driven" by the AppRTC
 * audio manager are not included in this test.
 */
@RunWith(LocalRobolectricTestRunner.clreplaced)
@Config(manifest = Config.NONE)
public clreplaced BluetoothManagerTest {

    private static final String TAG = "BluetoothManagerTest";

    private static final String BLUETOOTH_TEST_DEVICE_NAME = "BluetoothTestDevice";

    private BroadcastReceiver bluetoothHeadsetStateReceiver;

    private BluetoothProfile.ServiceListener bluetoothServiceListener;

    private BluetoothHeadset mockedBluetoothHeadset;

    private BluetoothDevice mockedBluetoothDevice;

    private List<BluetoothDevice> mockedBluetoothDeviceList;

    private AppRTCBluetoothManager bluetoothManager;

    private AppRTCAudioManager mockedAppRtcAudioManager;

    private AudioManager mockedAudioManager;

    private Context context;

    @Before
    public void setUp() {
        ShadowLog.stream = System.out;
        context = ShadowApplication.getInstance().getApplicationContext();
        mockedAppRtcAudioManager = mock(AppRTCAudioManager.clreplaced);
        mockedAudioManager = mock(AudioManager.clreplaced);
        mockedBluetoothHeadset = mock(BluetoothHeadset.clreplaced);
        mockedBluetoothDevice = mock(BluetoothDevice.clreplaced);
        mockedBluetoothDeviceList = new LinkedList<BluetoothDevice>();
        // Simulate that bluetooth SCO audio is available by default.
        when(mockedAudioManager.isBluetoothScoAvailableOffCall()).thenReturn(true);
        // Create the test object and override protected methods for this test.
        bluetoothManager = new AppRTCBluetoothManager(context, mockedAppRtcAudioManager) {

            @Override
            protected AudioManager getAudioManager(Context context) {
                Log.d(TAG, "getAudioManager");
                return mockedAudioManager;
            }

            @Override
            protected void registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
                Log.d(TAG, "registerReceiver");
                if (filter.hasAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED) && filter.hasAction(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED)) {
                    // Gives access to the real broadcast receiver so the test can use it.
                    bluetoothHeadsetStateReceiver = receiver;
                }
            }

            @Override
            protected void unregisterReceiver(BroadcastReceiver receiver) {
                Log.d(TAG, "unregisterReceiver");
                if (receiver == bluetoothHeadsetStateReceiver) {
                    bluetoothHeadsetStateReceiver = null;
                }
            }

            @Override
            protected boolean getBluetoothProfileProxy(Context context, BluetoothProfile.ServiceListener listener, int profile) {
                Log.d(TAG, "getBluetoothProfileProxy");
                if (profile == BluetoothProfile.HEADSET) {
                    // Allows the test to access the real Bluetooth service listener object.
                    bluetoothServiceListener = listener;
                }
                return true;
            }

            @Override
            protected boolean hasPermission(Context context, String permission) {
                Log.d(TAG, "hasPermission(" + permission + ")");
                // Ensure that the client asks for Bluetooth permission.
                return (permission == android.Manifest.permission.BLUETOOTH);
            }

            @Override
            protected void logBluetoothAdapterInfo(BluetoothAdapter localAdapter) {
            // Do nothing in tests. No need to mock BluetoothAdapter.
            }
        };
    }

    // Verify that Bluetooth service listener for headset profile is properly initialized.
    @Test
    public void testBluetoothServiceListenerInitialized() {
        bluetoothManager.start();
        replacedertNotNull(bluetoothServiceListener);
        verify(mockedAppRtcAudioManager, never()).updateAudioDeviceState();
    }

    // Verify that broadcast receivers for Bluetooth SCO audio state and Bluetooth headset state
    // are properly registered and unregistered.
    @Test
    public void testBluetoothBroadcastReceiversAreRegistered() {
        bluetoothManager.start();
        replacedertNotNull(bluetoothHeadsetStateReceiver);
        bluetoothManager.stop();
        replacedertNull(bluetoothHeadsetStateReceiver);
    }

    // Verify that the Bluetooth manager starts and stops with correct states.
    @Test
    public void testBluetoothDefaultStartStopStates() {
        bluetoothManager.start();
        replacedertEquals(bluetoothManager.getState(), State.HEADSET_UNAVAILABLE);
        bluetoothManager.stop();
        replacedertEquals(bluetoothManager.getState(), State.UNINITIALIZED);
    }

    // Verify correct state after receiving BluetoothServiceListener.onServiceConnected()
    // when no BT device is enabled.
    @Test
    public void testBluetoothServiceListenerConnectedWithNoHeadset() {
        bluetoothManager.start();
        replacedertEquals(bluetoothManager.getState(), State.HEADSET_UNAVAILABLE);
        simulateBluetoothServiceConnectedWithNoConnectedHeadset();
        verify(mockedAppRtcAudioManager, times(1)).updateAudioDeviceState();
        replacedertEquals(bluetoothManager.getState(), State.HEADSET_UNAVAILABLE);
    }

    // Verify correct state after receiving BluetoothServiceListener.onServiceConnected()
    // when one emulated (test) BT device is enabled. Android does not support more than
    // one connected BT headset.
    @Test
    public void testBluetoothServiceListenerConnectedWithHeadset() {
        bluetoothManager.start();
        replacedertEquals(bluetoothManager.getState(), State.HEADSET_UNAVAILABLE);
        simulateBluetoothServiceConnectedWithConnectedHeadset();
        verify(mockedAppRtcAudioManager, times(1)).updateAudioDeviceState();
        replacedertEquals(bluetoothManager.getState(), State.HEADSET_AVAILABLE);
    }

    // Verify correct state after receiving BluetoothProfile.ServiceListener.onServiceDisconnected().
    @Test
    public void testBluetoothServiceListenerDisconnected() {
        bluetoothManager.start();
        replacedertEquals(bluetoothManager.getState(), State.HEADSET_UNAVAILABLE);
        simulateBluetoothServiceDisconnected();
        verify(mockedAppRtcAudioManager, times(1)).updateAudioDeviceState();
        replacedertEquals(bluetoothManager.getState(), State.HEADSET_UNAVAILABLE);
    }

    // Verify correct state after BluetoothServiceListener.onServiceConnected() and
    // the intent indicating that the headset is actually connected. Both these callbacks
    // results in calls to updateAudioDeviceState() on the AppRTC audio manager.
    // No BT SCO is enabled here to keep the test limited.
    @Test
    public void testBluetoothHeadsetConnected() {
        bluetoothManager.start();
        replacedertEquals(bluetoothManager.getState(), State.HEADSET_UNAVAILABLE);
        simulateBluetoothServiceConnectedWithConnectedHeadset();
        simulateBluetoothHeadsetConnected();
        verify(mockedAppRtcAudioManager, times(2)).updateAudioDeviceState();
        replacedertEquals(bluetoothManager.getState(), State.HEADSET_AVAILABLE);
    }

    // Verify correct state sequence for a case when a BT headset is available,
    // followed by BT SCO audio being enabled and then stopped.
    @Test
    public void testBluetoothScoAudioStartAndStop() {
        bluetoothManager.start();
        replacedertEquals(bluetoothManager.getState(), State.HEADSET_UNAVAILABLE);
        simulateBluetoothServiceConnectedWithConnectedHeadset();
        replacedertEquals(bluetoothManager.getState(), State.HEADSET_AVAILABLE);
        bluetoothManager.startScoAudio();
        replacedertEquals(bluetoothManager.getState(), State.SCO_CONNECTING);
        simulateBluetoothScoConnectionConnected();
        replacedertEquals(bluetoothManager.getState(), State.SCO_CONNECTED);
        bluetoothManager.stopScoAudio();
        simulateBluetoothScoConnectionDisconnected();
        replacedertEquals(bluetoothManager.getState(), State.SCO_DISCONNECTING);
        bluetoothManager.stop();
        replacedertEquals(bluetoothManager.getState(), State.UNINITIALIZED);
        verify(mockedAppRtcAudioManager, times(3)).updateAudioDeviceState();
    }

    /**
     * Private helper methods.
     */
    private void simulateBluetoothServiceConnectedWithNoConnectedHeadset() {
        mockedBluetoothDeviceList.clear();
        when(mockedBluetoothHeadset.getConnectedDevices()).thenReturn(mockedBluetoothDeviceList);
        bluetoothServiceListener.onServiceConnected(BluetoothProfile.HEADSET, mockedBluetoothHeadset);
        // In real life, the AppRTC audio manager makes this call.
        bluetoothManager.updateDevice();
    }

    private void simulateBluetoothServiceConnectedWithConnectedHeadset() {
        mockedBluetoothDeviceList.clear();
        mockedBluetoothDeviceList.add(mockedBluetoothDevice);
        when(mockedBluetoothHeadset.getConnectedDevices()).thenReturn(mockedBluetoothDeviceList);
        when(mockedBluetoothDevice.getName()).thenReturn(BLUETOOTH_TEST_DEVICE_NAME);
        bluetoothServiceListener.onServiceConnected(BluetoothProfile.HEADSET, mockedBluetoothHeadset);
        // In real life, the AppRTC audio manager makes this call.
        bluetoothManager.updateDevice();
    }

    private void simulateBluetoothServiceDisconnected() {
        bluetoothServiceListener.onServiceDisconnected(BluetoothProfile.HEADSET);
    }

    private void simulateBluetoothHeadsetConnected() {
        Intent intent = new Intent();
        intent.setAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
        intent.putExtra(BluetoothHeadset.EXTRA_STATE, BluetoothHeadset.STATE_CONNECTED);
        bluetoothHeadsetStateReceiver.onReceive(context, intent);
    }

    private void simulateBluetoothScoConnectionConnected() {
        Intent intent = new Intent();
        intent.setAction(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED);
        intent.putExtra(BluetoothHeadset.EXTRA_STATE, BluetoothHeadset.STATE_AUDIO_CONNECTED);
        bluetoothHeadsetStateReceiver.onReceive(context, intent);
    }

    private void simulateBluetoothScoConnectionDisconnected() {
        Intent intent = new Intent();
        intent.setAction(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED);
        intent.putExtra(BluetoothHeadset.EXTRA_STATE, BluetoothHeadset.STATE_AUDIO_DISCONNECTED);
        bluetoothHeadsetStateReceiver.onReceive(context, intent);
    }
}