Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 519af584 authored by Łukasz Rymanowski's avatar Łukasz Rymanowski Committed by Jakub Pawlowski
Browse files

settingslib/bluetooth: Add LeAudio support

Bug: 150670922
Tag: #feature
Sponsor: jpawlowski@
Test: Manual
Change-Id: I45402a6d315d8ccc2bc756bdc9937e81eea3c58a
parent a172c7fb
Loading
Loading
Loading
Loading
+31 −0
Original line number Diff line number Diff line
<!--
     Copyright 2021 The Android Open Source Project

     Licensed under the Apache License, Version 2.0 (the "License");
     you may not use this file except in compliance with the License.
     You may obtain a copy of the License at

          http://www.apache.org/licenses/LICENSE-2.0

     Unless required by applicable law or agreed to in writing, software
     distributed under the License is distributed on an "AS IS" BASIS,
     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     See the License for the specific language governing permissions and
     limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="24dp"
    android:height="24dp"
    android:viewportWidth="24.0"
    android:viewportHeight="24.0"
    android:tint="?android:attr/colorControlNormal" >
    <path
        android:pathData="M18.2,1L9.8,1C8.81,1 8,1.81 8,2.8v14.4c0,0.99 0.81,1.79 1.8,1.79l8.4,0.01c0.99,0 1.8,-0.81 1.8,-1.8L20,2.8c0,-0.99 -0.81,-1.8 -1.8,-1.8zM14,3c1.1,0 2,0.89 2,2s-0.9,2 -2,2 -2,-0.89 -2,-2 0.9,-2 2,-2zM14,16.5c-2.21,0 -4,-1.79 -4,-4s1.79,-4 4,-4 4,1.79 4,4 -1.79,4 -4,4z"
        android:fillColor="#FFFFFFFF"/>
    <path
        android:pathData="M14,12.5m-2.5,0a2.5,2.5 0,1 1,5 0a2.5,2.5 0,1 1,-5 0"
        android:fillColor="#FFFFFFFF"/>
    <path
        android:pathData="M6,5H4v16c0,1.1 0.89,2 2,2h10v-2H6V5z"
        android:fillColor="#FFFFFFFF"/>
</vector>
+6 −0
Original line number Diff line number Diff line
@@ -257,8 +257,12 @@

    <!-- Bluetooth settings.  The user-visible string that is used whenever referring to the Hearing Aid profile. -->
    <string name="bluetooth_profile_hearing_aid">Hearing Aids</string>
    <!-- Bluetooth settings.  The user-visible string that is used whenever referring to the LE_AUDIO profile. -->
    <string name="bluetooth_profile_le_audio">LE_AUDIO</string>
    <!-- Bluetooth settings.  Connection options screen.  The summary for the Hearing Aid checkbox preference when Hearing Aid is connected. -->
    <string name="bluetooth_hearing_aid_profile_summary_connected">Connected to Hearing Aids</string>
    <!-- Bluetooth settings.  Connection options screen.  The summary for the LE_AUDIO checkbox preference when LE_AUDIO is connected. -->
    <string name="bluetooth_le_audio_profile_summary_connected">Connected to LE_AUDIO</string>

    <!-- Bluetooth settings.  Connection options screen.  The summary for the A2DP checkbox preference when A2DP is connected. -->
    <string name="bluetooth_a2dp_profile_summary_connected">Connected to media audio</string>
@@ -299,6 +303,8 @@
    <string name="bluetooth_hid_profile_summary_use_for">Use for input</string>
    <!-- Bluetooth settings.  Connection options screen.  The summary for the Hearing Aid checkbox preference that describes how checking it will set the Hearing Aid profile as preferred. -->
    <string name="bluetooth_hearing_aid_profile_summary_use_for">Use for Hearing Aids</string>
    <!-- Bluetooth settings.  Connection options screen.  The summary for the LE_AUDIO checkbox preference that describes how checking it will set the LE_AUDIO profile as preferred. -->
    <string name="bluetooth_le_audio_profile_summary_use_for">Use for LE_AUDIO</string>

    <!-- Button text for accepting an incoming pairing request. [CHAR LIMIT=20] -->
    <string name="bluetooth_pairing_accept">Pair</string>
+6 −0
Original line number Diff line number Diff line
@@ -22,6 +22,7 @@ import android.bluetooth.BluetoothCsipSetCoordinator;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHeadset;
import android.bluetooth.BluetoothHearingAid;
import android.bluetooth.BluetoothLeAudio;
import android.bluetooth.BluetoothProfile;
import android.content.BroadcastReceiver;
import android.content.Context;
@@ -116,6 +117,8 @@ public class BluetoothEventManager {
        addHandler(BluetoothHeadset.ACTION_ACTIVE_DEVICE_CHANGED, new ActiveDeviceChangedHandler());
        addHandler(BluetoothHearingAid.ACTION_ACTIVE_DEVICE_CHANGED,
                new ActiveDeviceChangedHandler());
        addHandler(BluetoothLeAudio.ACTION_LE_AUDIO_ACTIVE_DEVICE_CHANGED,
                   new ActiveDeviceChangedHandler());

        // Headset state changed broadcasts
        addHandler(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED,
@@ -455,6 +458,9 @@ public class BluetoothEventManager {
                bluetoothProfile = BluetoothProfile.HEADSET;
            } else if (Objects.equals(action, BluetoothHearingAid.ACTION_ACTIVE_DEVICE_CHANGED)) {
                bluetoothProfile = BluetoothProfile.HEARING_AID;
            } else if (Objects.equals(action,
                        BluetoothLeAudio.ACTION_LE_AUDIO_ACTIVE_DEVICE_CHANGED)) {
                bluetoothProfile = BluetoothProfile.LE_AUDIO;
            } else {
                Log.w(TAG, "ActiveDeviceChangedHandler: unknown action " + action);
                return;
+51 −2
Original line number Diff line number Diff line
@@ -21,6 +21,7 @@ import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothCsipSetCoordinator;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHearingAid;
import android.bluetooth.BluetoothLeAudio;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothUuid;
import android.content.Context;
@@ -109,11 +110,14 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>
    private boolean mIsActiveDeviceA2dp = false;
    private boolean mIsActiveDeviceHeadset = false;
    private boolean mIsActiveDeviceHearingAid = false;
    private boolean mIsActiveDeviceLeAudio = false;
    // Media profile connect state
    private boolean mIsA2dpProfileConnectedFail = false;
    private boolean mIsHeadsetProfileConnectedFail = false;
    private boolean mIsHearingAidProfileConnectedFail = false;
    private boolean mIsLeAudioProfileConnectedFail = false;
    private boolean mUnpairing;

    // Group second device for Hearing Aid
    private CachedBluetoothDevice mSubDevice;
    // Group member devices for the coordinated set
@@ -134,6 +138,9 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>
                case BluetoothProfile.HEARING_AID:
                    mIsHearingAidProfileConnectedFail = true;
                    break;
                case BluetoothProfile.LE_AUDIO:
                    mIsLeAudioProfileConnectedFail = true;
                    break;
                default:
                    Log.w(TAG, "handleMessage(): unknown message : " + msg.what);
                    break;
@@ -268,6 +275,9 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>
            case BluetoothProfile.HEARING_AID:
                mIsHearingAidProfileConnectedFail = isFailed;
                break;
            case BluetoothProfile.LE_AUDIO:
                mIsLeAudioProfileConnectedFail = isFailed;
                break;
            default:
                Log.w(TAG, "setProfileConnectedStatus(): unknown profile id : " + profileId);
                break;
@@ -544,6 +554,13 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>
                result = true;
            }
        }
        LeAudioProfile leAudioProfile = mProfileManager.getLeAudioProfile();
        if ((leAudioProfile != null) && isConnectedProfile(leAudioProfile)) {
            if (leAudioProfile.setActiveDevice(getDevice())) {
                Log.i(TAG, "OnPreferenceClickListener: LeAudio active device=" + this);
                result = true;
            }
        }
        return result;
    }

@@ -622,6 +639,10 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>
            changed = (mIsActiveDeviceHearingAid != isActive);
            mIsActiveDeviceHearingAid = isActive;
            break;
        case BluetoothProfile.LE_AUDIO:
            changed = (mIsActiveDeviceLeAudio != isActive);
            mIsActiveDeviceLeAudio = isActive;
            break;
        default:
            Log.w(TAG, "onActiveDeviceChanged: unknown profile " + bluetoothProfile +
                    " isActive " + isActive);
@@ -653,6 +674,8 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>
                return mIsActiveDeviceHeadset;
            case BluetoothProfile.HEARING_AID:
                return mIsActiveDeviceHearingAid;
            case BluetoothProfile.LE_AUDIO:
                return mIsActiveDeviceLeAudio;
            default:
                Log.w(TAG, "getActiveDevice: unknown profile " + bluetoothProfile);
                break;
@@ -747,6 +770,10 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>
        if (hearingAidProfile != null) {
            mIsActiveDeviceHearingAid = hearingAidProfile.getActiveDevices().contains(mDevice);
        }
        LeAudioProfile leAudio = mProfileManager.getLeAudioProfile();
        if (leAudio != null) {
            mIsActiveDeviceLeAudio = leAudio.getActiveDevices().contains(mDevice);
        }
    }

    /**
@@ -987,6 +1014,7 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>
        boolean a2dpConnected = true;        // A2DP is connected
        boolean hfpConnected = true;         // HFP is connected
        boolean hearingAidConnected = true;  // Hearing Aid is connected
        boolean leAudioConnected = true;        // LeAudio is connected
        int leftBattery = -1;
        int rightBattery = -1;

@@ -1018,6 +1046,8 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>
                                hfpConnected = false;
                            } else if (profile instanceof HearingAidProfile) {
                                hearingAidConnected = false;
                            } else if (profile instanceof LeAudioProfile) {
                                leAudioConnected = false;
                            }
                        }
                        break;
@@ -1060,7 +1090,8 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>
            //    1. Hearing Aid device active.
            //    2. Headset device active with in-calling state.
            //    3. A2DP device active without in-calling state.
            if (a2dpConnected || hfpConnected || hearingAidConnected) {
            //    4. Le Audio device active
            if (a2dpConnected || hfpConnected || hearingAidConnected || leAudioConnected) {
                final boolean isOnCall = Utils.isAudioModeOngoingCall(mContext);
                if ((mIsActiveDeviceHearingAid)
                        || (mIsActiveDeviceHeadset && isOnCall)
@@ -1095,7 +1126,8 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>

    private boolean isProfileConnectedFail() {
        return mIsA2dpProfileConnectedFail || mIsHearingAidProfileConnectedFail
                || (!isConnectedSapDevice() && mIsHeadsetProfileConnectedFail);
                || (!isConnectedSapDevice() && mIsHeadsetProfileConnectedFail)
                || mIsLeAudioProfileConnectedFail;
    }

    /**
@@ -1106,6 +1138,7 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>
        boolean a2dpNotConnected = false;       // A2DP is preferred but not connected
        boolean hfpNotConnected = false;        // HFP is preferred but not connected
        boolean hearingAidNotConnected = false; // Hearing Aid is preferred but not connected
        boolean leAudioNotConnected = false;       // LeAudio is preferred but not connected

        synchronized (mProfileLock) {
            for (LocalBluetoothProfile profile : getProfiles()) {
@@ -1131,6 +1164,8 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>
                                hfpNotConnected = true;
                            } else if (profile instanceof HearingAidProfile) {
                                hearingAidNotConnected = true;
                            } else if (profile instanceof  LeAudioProfile) {
                                leAudioNotConnected = true;
                            }
                        }
                        break;
@@ -1169,6 +1204,11 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>
            return mContext.getString(R.string.bluetooth_connected, activeDeviceString);
        }

        if (!leAudioNotConnected && mIsActiveDeviceLeAudio) {
            activeDeviceString = activeDeviceStringsArray[1];
            return mContext.getString(R.string.bluetooth_connected, activeDeviceString);
        }

        if (profileConnected) {
            if (a2dpNotConnected && hfpNotConnected) {
                if (batteryLevelPercentageString != null) {
@@ -1238,6 +1278,15 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>
                BluetoothProfile.STATE_CONNECTED;
    }

    /**
     * @return {@code true} if {@code cachedBluetoothDevice} is LeAudio device
     */
    public boolean isConnectedLeAudioDevice() {
        LeAudioProfile leAudio = mProfileManager.getLeAudioProfile();
        return leAudio != null && leAudio.getConnectionStatus(mDevice) ==
                BluetoothProfile.STATE_CONNECTED;
    }

    private boolean isConnectedSapDevice() {
        SapProfile sapProfile = mProfileManager.getSapProfile();
        return sapProfile != null && sapProfile.getConnectionStatus(mDevice)
+264 −0
Original line number Diff line number Diff line
/*   Copyright 2021 HIMSA II K/S - www.himsa.com. Represented by EHIMA
- www.ehima.com
*/

/* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*     http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.android.settingslib.bluetooth;

import static android.bluetooth.BluetoothAdapter.ACTIVE_DEVICE_ALL;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;

import android.bluetooth.BluetoothLeAudio;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothCodecConfig;
import android.bluetooth.BluetoothCodecStatus;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothUuid;
import android.content.Context;
import android.os.Build;
import android.os.ParcelUuid;
import android.util.Log;

import androidx.annotation.RequiresApi;

import com.android.internal.annotations.VisibleForTesting;
import com.android.settingslib.R;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class LeAudioProfile implements LocalBluetoothProfile {
    private static final String TAG = "LeAudioProfile";
    private static boolean DEBUG = true;

    private Context mContext;

    private BluetoothLeAudio mService;
    private boolean mIsProfileReady;

    private final CachedBluetoothDeviceManager mDeviceManager;

    static final String NAME = "LE_AUDIO";
    private final LocalBluetoothProfileManager mProfileManager;
    private final BluetoothAdapter mBluetoothAdapter;

    // Order of this profile in device profiles list
    private static final int ORDINAL = 1;

    // These callbacks run on the main thread.
    private final class LeAudioServiceListener
            implements BluetoothProfile.ServiceListener {

        @RequiresApi(Build.VERSION_CODES.S)
        public void onServiceConnected(int profile, BluetoothProfile proxy) {
            if (DEBUG) {
                Log.d(TAG,"Bluetooth service connected");
            }
            mService = (BluetoothLeAudio) proxy;
            // We just bound to the service, so refresh the UI for any connected LeAudio devices.
            List<BluetoothDevice> deviceList = mService.getConnectedDevices();
            while (!deviceList.isEmpty()) {
                BluetoothDevice nextDevice = deviceList.remove(0);
                CachedBluetoothDevice device = mDeviceManager.findDevice(nextDevice);
                // we may add a new device here, but generally this should not happen
                if (device == null) {
                    if (DEBUG) {
                        Log.d(TAG, "LeAudioProfile found new device: " + nextDevice);
                    }
                    device = mDeviceManager.addDevice(nextDevice);
                }
                device.onProfileStateChanged(LeAudioProfile.this,
                        BluetoothProfile.STATE_CONNECTED);
                device.refresh();
            }

            mProfileManager.callServiceConnectedListeners();
            mIsProfileReady = true;
        }

        public void onServiceDisconnected(int profile) {
            if (DEBUG) {
                 Log.d(TAG,"Bluetooth service disconnected");
            }
            mProfileManager.callServiceDisconnectedListeners();
            mIsProfileReady = false;
        }
    }

    public boolean isProfileReady() {
        return mIsProfileReady;
    }

    @Override
    public int getProfileId() {
        return BluetoothProfile.LE_AUDIO;
    }

    LeAudioProfile(Context context, CachedBluetoothDeviceManager deviceManager,
            LocalBluetoothProfileManager profileManager) {
        mContext = context;
        mDeviceManager = deviceManager;
        mProfileManager = profileManager;

        mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
        mBluetoothAdapter.getProfileProxy(
                context, new LeAudioServiceListener(),
                BluetoothProfile.LE_AUDIO);
    }

    public boolean accessProfileEnabled() {
        return true;
    }

    public boolean isAutoConnectable() {
        return true;
    }

    public List<BluetoothDevice> getConnectedDevices() {
        if (mService == null) {
            return new ArrayList<BluetoothDevice>(0);
        }
        return mService.getDevicesMatchingConnectionStates(
              new int[] {BluetoothProfile.STATE_CONNECTED,
                         BluetoothProfile.STATE_CONNECTING,
                         BluetoothProfile.STATE_DISCONNECTING});
    }

    /*
    * @hide
    */
    public boolean connect(BluetoothDevice device) {
       if (mService == null) {
           return false;
       }
       return mService.connect(device);
    }

    /*
    * @hide
    */
    public boolean disconnect(BluetoothDevice device) {
       if (mService == null) {
           return false;
       }
       return mService.disconnect(device);
    }

    public int getConnectionStatus(BluetoothDevice device) {
        if (mService == null) {
            return BluetoothProfile.STATE_DISCONNECTED;
        }
        return mService.getConnectionState(device);
    }

    public boolean setActiveDevice(BluetoothDevice device) {
        if (mBluetoothAdapter == null) {
            return false;
        }
        return device == null
                ? mBluetoothAdapter.removeActiveDevice(ACTIVE_DEVICE_ALL)
                : mBluetoothAdapter.setActiveDevice(device, ACTIVE_DEVICE_ALL);
    }

    public List<BluetoothDevice> getActiveDevices() {
        if (mService == null) {
            return new ArrayList<>();
        }
        return mService.getActiveDevices();
    }

    @Override
    public boolean isEnabled(BluetoothDevice device) {
        if (mService == null || device == null) {
            return false;
        }
        return mService.getConnectionPolicy(device) > CONNECTION_POLICY_FORBIDDEN;
    }

    @Override
    public int getConnectionPolicy(BluetoothDevice device) {
        if (mService == null || device == null) {
            return CONNECTION_POLICY_FORBIDDEN;
        }
        return mService.getConnectionPolicy(device);
    }

    @Override
    public boolean setEnabled(BluetoothDevice device, boolean enabled) {
        boolean isEnabled = false;
        if (mService == null || device == null) {
            return false;
        }
        if (enabled) {
            if (mService.getConnectionPolicy(device) < CONNECTION_POLICY_ALLOWED) {
                isEnabled = mService.setConnectionPolicy(device, CONNECTION_POLICY_ALLOWED);
            }
        } else {
            isEnabled = mService.setConnectionPolicy(device, CONNECTION_POLICY_FORBIDDEN);
        }

        return isEnabled;
    }

    public String toString() {
        return NAME;
    }

    public int getOrdinal() {
        return ORDINAL;
    }

    public int getNameResource(BluetoothDevice device) {
        return R.string.bluetooth_profile_le_audio;
    }

    public int getSummaryResourceForDevice(BluetoothDevice device) {
        int state = getConnectionStatus(device);
        switch (state) {
            case BluetoothProfile.STATE_DISCONNECTED:
                return R.string.bluetooth_le_audio_profile_summary_use_for;

            case BluetoothProfile.STATE_CONNECTED:
                return R.string.bluetooth_le_audio_profile_summary_connected;

            default:
                return BluetoothUtils.getConnectionStateSummary(state);
        }
    }

    public int getDrawableResource(BluetoothClass btClass) {
        return R.drawable.ic_bt_le_audio;
    }

    @RequiresApi(Build.VERSION_CODES.S)
    protected void finalize() {
        if (DEBUG) {
            Log.d(TAG, "finalize()");
        }
        if (mService != null) {
            try {
                BluetoothAdapter.getDefaultAdapter().closeProfileProxy(BluetoothProfile.LE_AUDIO,
                        mService);
                mService = null;
            }catch (Throwable t) {
                Log.w(TAG, "Error cleaning up LeAudio proxy", t);
            }
        }
    }
}
Loading