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

Commit 1498f9a2 authored by Rongxuan Liu's avatar Rongxuan Liu Committed by Automerger Merge Worker
Browse files

Merge "[le audio] Implement public broadcast for broadcast assistant" am: a62e45d3

parents 2886c6c2 a62e45d3
Loading
Loading
Loading
Loading
+11 −2
Original line number Diff line number Diff line
@@ -120,13 +120,16 @@ public class BassClientService extends ProfileService {
            int syncHandle,
            int advSid,
            int advInterval,
            int bId) {
            int bId,
            PublicBroadcastData pbData,
            String broadcastName) {
        log("updatePeriodicAdvertisementResultMap: device: " + device);
        log("updatePeriodicAdvertisementResultMap: syncHandle: " + syncHandle);
        log("updatePeriodicAdvertisementResultMap: advSid: " + advSid);
        log("updatePeriodicAdvertisementResultMap: addressType: " + addressType);
        log("updatePeriodicAdvertisementResultMap: advInterval: " + advInterval);
        log("updatePeriodicAdvertisementResultMap: broadcastId: " + bId);
        log("updatePeriodicAdvertisementResultMap: broadcastName: " + broadcastName);
        log("mDeviceToSyncHandleMap" + mDeviceToSyncHandleMap);
        log("mPeriodicAdvertisementResultMap" + mPeriodicAdvertisementResultMap);
        // Cache the SyncHandle
@@ -138,7 +141,7 @@ public class BassClientService extends ProfileService {
            if (paRes == null) {
                log("PAResmap: add >>>");
                paRes = new PeriodicAdvertisementResult(device,
                        addressType, syncHandle, advSid, advInterval, bId);
                        addressType, syncHandle, advSid, advInterval, bId, pbData, broadcastName);
                if (paRes != null) {
                    paRes.print();
                    mPeriodicAdvertisementResultMap.put(device, paRes);
@@ -159,6 +162,12 @@ public class BassClientService extends ProfileService {
                if (bId != BassConstants.INVALID_BROADCAST_ID) {
                    paRes.updateBroadcastId(bId);
                }
                if (pbData != null) {
                    paRes.updatePublicBroadcastData(pbData);
                }
                if (broadcastName != null) {
                    paRes.updateBroadcastName(broadcastName);
                }
                log("PAResmap: update >>>");
                paRes.print();
                mPeriodicAdvertisementResultMap.replace(device, paRes);
+59 −2
Original line number Diff line number Diff line
@@ -35,6 +35,8 @@ import android.bluetooth.BluetoothLeBroadcastReceiveState;
import android.bluetooth.BluetoothLeBroadcastSubgroup;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothStatusCodes;
import android.bluetooth.BluetoothUtils;
import android.bluetooth.BluetoothUtils.TypeValueEntry;
import android.bluetooth.le.PeriodicAdvertisingCallback;
import android.bluetooth.le.PeriodicAdvertisingManager;
import android.bluetooth.le.PeriodicAdvertisingReport;
@@ -60,6 +62,7 @@ import java.io.ByteArrayOutputStream;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@@ -347,6 +350,32 @@ public class BassClientStateMachine extends StateMachine {
        }
    }

    private String checkAndParseBroadcastName(ScanRecord record) {
        log("checkAndParseBroadcastName");
        byte[] rawBytes = record.getBytes();
        List<TypeValueEntry> entries = BluetoothUtils.parseLengthTypeValueBytes(rawBytes);
        if (rawBytes.length > 0 && rawBytes[0] > 0 && entries.isEmpty()) {
            Log.e(TAG, "Invalid LTV entries in Scan record");
            return null;
        }

        String broadcastName = null;
        for (TypeValueEntry entry : entries) {
            // Only use the first value of each type
            if (broadcastName == null && entry.getType() == BassConstants.BCAST_NAME_AD_TYPE) {
                byte[] bytes = entry.getValue();
                int len = bytes.length;
                if (len < BassConstants.BCAST_NAME_LEN_MIN
                        || len > BassConstants.BCAST_NAME_LEN_MAX) {
                    Log.e(TAG, "Invalid broadcast name length in Scan record" + len);
                    return null;
                }
                broadcastName = new String(bytes, StandardCharsets.UTF_8);
            }
        }
        return broadcastName;
    }

    private boolean selectSource(
            ScanResult scanRes, boolean autoTriggered) {
        log("selectSource: ScanResult " + scanRes);
@@ -375,19 +404,31 @@ public class BassClientStateMachine extends StateMachine {
        if (scanRecord != null) {
            Map<ParcelUuid, byte[]> listOfUuids = scanRecord.getServiceData();
            int broadcastId = BassConstants.INVALID_BROADCAST_ID;
            PublicBroadcastData pbData = null;
            if (listOfUuids != null) {
                if (listOfUuids.containsKey(BassConstants.BAAS_UUID)) {
                    byte[] bId = listOfUuids.get(BassConstants.BAAS_UUID);
                    broadcastId = BassUtils.parseBroadcastId(bId);
                }
                if (listOfUuids.containsKey(BassConstants.PUBLIC_BROADCAST_UUID)) {
                    byte[] pbAnnouncement =
                            listOfUuids.get(BassConstants.PUBLIC_BROADCAST_UUID);
                    pbData = PublicBroadcastData.parsePublicBroadcastData(pbAnnouncement);
                }
            }
            // Check if broadcast name present in scan record and parse
            // null if no name present
            String broadcastName = checkAndParseBroadcastName(scanRecord);

            mService.updatePeriodicAdvertisementResultMap(
                    scanRes.getDevice(),
                    scanRes.getDevice().getAddressType(),
                    BassConstants.INVALID_SYNC_HANDLE,
                    BassConstants.INVALID_ADV_SID,
                    scanRes.getPeriodicAdvertisingInterval(),
                    broadcastId);
                    broadcastId,
                    pbData,
                    broadcastName);
        }
        return true;
    }
@@ -466,6 +507,19 @@ public class BassClientStateMachine extends StateMachine {
            log("broadcast ID: " + broadcastId);
            metaData.setBroadcastId(broadcastId);
            metaData.setSourceAdvertisingSid(result.getAdvSid());

            PublicBroadcastData pbData = result.getPublicBroadcastData();
            if (pbData != null) {
                metaData.setPublicBroadcast(true);
                metaData.setAudioConfigQuality(pbData.getAudioConfigQuality());
                metaData.setPublicBroadcastMetadata(BluetoothLeAudioContentMetadata
                        .fromRawBytes(pbData.getMetadata()));
            }

            String broadcastName = result.getBroadcastName();
            if (broadcastName != null) {
                metaData.setBroadcastName(broadcastName);
            }
        }
        metaData.setEncrypted(encrypted);
        return metaData.build();
@@ -490,13 +544,16 @@ public class BassClientStateMachine extends StateMachine {
                            + ", status: " + status);
                    if (status == BluetoothGatt.GATT_SUCCESS) {
                        // updates syncHandle, advSid
                        // set other fields as invalid or null
                        mService.updatePeriodicAdvertisementResultMap(
                                device,
                                BassConstants.INVALID_ADV_ADDRESS_TYPE,
                                syncHandle,
                                advertisingSid,
                                BassConstants.INVALID_ADV_INTERVAL,
                                BassConstants.INVALID_BROADCAST_ID);
                                BassConstants.INVALID_BROADCAST_ID,
                                null,
                                null);
                        sendMessageDelayed(PSYNC_ACTIVE_TIMEOUT,
                                BassConstants.PSYNC_ACTIVE_TIMEOUT_MS);
                        mService.setActiveSyncedSource(mDevice, device);
+6 −0
Original line number Diff line number Diff line
@@ -37,6 +37,8 @@ public class BassConstants {
            UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");
    public static final ParcelUuid BASIC_AUDIO_UUID =
            ParcelUuid.fromString("00001851-0000-1000-8000-00805F9B34FB");
    public static final ParcelUuid PUBLIC_BROADCAST_UUID =
            ParcelUuid.fromString("00001856-0000-1000-8000-00805F9B34FB");

    public static final int INVALID_SYNC_HANDLE = -1;
    public static final int INVALID_ADV_SID = -1;
@@ -71,4 +73,8 @@ public class BassConstants {
    public static final int PSYNC_TIMEOUT = 200;
    public static final int PIN_CODE_CMD_LEN = 18;
    public static final int CONNECT_TIMEOUT_MS = 30000;
    // broadcast name AD type and length
    public static final int BCAST_NAME_AD_TYPE = 0x30;
    public static final int BCAST_NAME_LEN_MIN = 4;
    public static final int BCAST_NAME_LEN_MAX = 32;
}
+41 −1
Original line number Diff line number Diff line
@@ -32,13 +32,17 @@ public class PeriodicAdvertisementResult {
    private int mPAInterval;
    private int mBroadcastId;
    private boolean mIsNotified;
    private PublicBroadcastData mPbData;
    private String mBroadcastName;

    PeriodicAdvertisementResult(BluetoothDevice device,
                                int addressType,
                                int syncHandle,
                                int advSid,
                                int paInterval,
                                int broadcastId) {
                                int broadcastId,
                                PublicBroadcastData pbData,
                                String broadcastName) {
        mDevice = device;
        mAddressType = addressType;
        mAdvSid = advSid;
@@ -46,6 +50,8 @@ public class PeriodicAdvertisementResult {
        mPAInterval = paInterval;
        mBroadcastId = broadcastId;
        mIsNotified = false;
        mPbData = pbData;
        mBroadcastName = broadcastName;
    }

    /**
@@ -133,6 +139,34 @@ public class PeriodicAdvertisementResult {
        return mBroadcastId;
    }

    /**
     * Update public broadcast data
     */
    public void updatePublicBroadcastData(PublicBroadcastData pbData) {
        mPbData = pbData;
    }

    /**
     * Get public broadcast data
     */
    public PublicBroadcastData getPublicBroadcastData() {
        return mPbData;
    }

    /**
     * Update broadcast name
     */
    public void updateBroadcastName(String broadcastName) {
        mBroadcastName = broadcastName;
    }

    /**
     * Get broadcast name
     */
    public String getBroadcastName() {
        return mBroadcastName;
    }

    /**
     * print
     */
@@ -145,7 +179,13 @@ public class PeriodicAdvertisementResult {
        log("mPAInterval:" + mPAInterval);
        log("mBroadcastId:" + mBroadcastId);
        log("mIsNotified: " + mIsNotified);
        log("mBroadcastName: " + mBroadcastName);
        log("-- END: PeriodicAdvertisementResult --");
        if (mPbData != null) {
            mPbData.print();
        } else {
            log("no public announcement present");
        }
    }

    static void log(String msg) {
+134 −0
Original line number Diff line number Diff line
/*
 * Copyright 2023 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.
 */

package com.android.bluetooth.bass_client;

import android.bluetooth.BluetoothLeBroadcastMetadata;
import android.util.Log;

import java.util.Arrays;

/**
 * Helper class to parse the Public Broadcast Announcement data
 */
class PublicBroadcastData {
    private static final String TAG = "Bassclient.PublicBroadcastData";
    private static final int FEATURES_ENCRYPTION_BIT = 0x01 << 0;
    private static final int FEATURES_STANDARD_QUALITY_BIT = 0x01 << 1;
    private static final int FEATURES_HIGH_QUALITY_BIT = 0x01 << 2;
    // public announcement service data should at least include features and metadata length
    private static final int PUBLIC_BROADCAST_SERVICE_DATA_LEN_MIN = 2;

    private final PublicBroadcastInfo mPublicBroadcastInfo;

    public static class PublicBroadcastInfo {
        public byte metaDataLength;
        public byte[] metaData;
        public boolean isEncrypted;
        public int audioConfigQuality;

        PublicBroadcastInfo() {
            metaDataLength = 0;
            metaData = new byte[0];
            isEncrypted = false;
            audioConfigQuality = BluetoothLeBroadcastMetadata.AUDIO_CONFIG_QUALITY_NONE;
            log("PublicBroadcastInfo is Initialized");
        }

        void print() {
            log("**BEGIN: Public Broadcast Information**");
            log("encrypted: " + isEncrypted);
            log("audio config quality: " + audioConfigQuality);
            log("metaDataLength: " + metaDataLength);
            if (metaDataLength != (byte) 0) {
                log("metaData: " + Arrays.toString(metaData));
            }
            log("**END: Public Broadcast Information****");
        }
    }

    PublicBroadcastData(PublicBroadcastInfo publicBroadcastInfo) {
        mPublicBroadcastInfo = publicBroadcastInfo;
    }

    static PublicBroadcastData parsePublicBroadcastData(byte[] serviceData) {
        if (serviceData == null || serviceData.length < PUBLIC_BROADCAST_SERVICE_DATA_LEN_MIN) {
            Log.e(TAG, "Invalid service data for PublicBroadcastData construction");
            throw new IllegalArgumentException("PublicBroadcastData: serviceData is invalid");
        }
        PublicBroadcastInfo publicBroadcastInfo = new PublicBroadcastInfo();

        log("PublicBroadcast input" + Arrays.toString(serviceData));

        int offset = 0;
        // Parse Public broadcast announcement features
        int features = serviceData[offset++];
        publicBroadcastInfo.isEncrypted =
                ((features & FEATURES_ENCRYPTION_BIT) != 0) ? true : false;
        publicBroadcastInfo.audioConfigQuality =
                BluetoothLeBroadcastMetadata.AUDIO_CONFIG_QUALITY_NONE;
        if ((features & FEATURES_STANDARD_QUALITY_BIT) != 0) {
            publicBroadcastInfo.audioConfigQuality |=
                    BluetoothLeBroadcastMetadata.AUDIO_CONFIG_QUALITY_STANDARD;
        }
        if ((features & FEATURES_HIGH_QUALITY_BIT) != 0) {
            publicBroadcastInfo.audioConfigQuality |=
                    BluetoothLeBroadcastMetadata.AUDIO_CONFIG_QUALITY_HIGH;
        }

        // Parse Public broadcast announcement metadata
        publicBroadcastInfo.metaDataLength = serviceData[offset++];
        if (serviceData.length
                != (publicBroadcastInfo.metaDataLength + PUBLIC_BROADCAST_SERVICE_DATA_LEN_MIN)) {
            Log.e(TAG, "Invalid meta data length for PublicBroadcastData construction");
            throw new IllegalArgumentException("PublicBroadcastData: metaData is invalid");
        }
        if (publicBroadcastInfo.metaDataLength != 0) {
            publicBroadcastInfo.metaData = new byte[(int) publicBroadcastInfo.metaDataLength];
            System.arraycopy(serviceData, offset,
                    publicBroadcastInfo.metaData, 0, (int) publicBroadcastInfo.metaDataLength);
            offset += publicBroadcastInfo.metaDataLength;
        }
        publicBroadcastInfo.print();
        return new PublicBroadcastData(publicBroadcastInfo);
    }

    boolean isEncrypted() {
        return mPublicBroadcastInfo.isEncrypted;
    }

    int getAudioConfigQuality() {
        return mPublicBroadcastInfo.audioConfigQuality;
    }

    int getMetadataLength() {
        return mPublicBroadcastInfo.metaDataLength;
    }

    byte[] getMetadata() {
        return mPublicBroadcastInfo.metaData;
    }

    void print() {
        mPublicBroadcastInfo.print();
    }

    static void log(String msg) {
        if (BassConstants.BASS_DBG) {
            Log.d(TAG, msg);
        }
    }
}
Loading