[LE Audio] New logic to generate the QR code

Currently the QR code string is generated in SettingsLib, and use
"<" and ">" to format the string.

Because the formatted string are multi layer nested, like
A:<B:<C:<<>;D:<>>;>;E:<F:<B:>;>;>;
Using Regex cannot handle this case,
- Regex cannot tell which B: is label for filed, which B: is user input
- Regex cannot tell which pair of <> is the correct open and close for B
- And these is no easy fix

The BluetoothLeBroadcastMetadata already implements the Parcelable
interface, use Parcel to serialize the string to fix issues.

Also, add unit test.

Fix: 248409874
Test: Unit test
Change-Id: I31ce4bb4f5e639deb0cb60ebd7d6bf904ae85788
diff --git a/packages/SettingsLib/TEST_MAPPING b/packages/SettingsLib/TEST_MAPPING
new file mode 100644
index 0000000..f6ada4c1a
--- /dev/null
+++ b/packages/SettingsLib/TEST_MAPPING
@@ -0,0 +1,13 @@
+{
+  "presubmit": [
+    {
+      "name": "SettingsLibUnitTests"
+    },
+    {
+      "name": "SpaPrivilegedLibTests"
+    },
+    {
+      "name": "SettingsSpaUnitTests"
+    }
+  ]
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothBroadcastUtils.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothBroadcastUtils.java
index a80061e..2bca7cf 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothBroadcastUtils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothBroadcastUtils.java
@@ -43,39 +43,5 @@
     /**
      * Bluetooth scheme.
      */
-    public static final String SCHEME_BT_BROADCAST_METADATA = "BT:";
-
-    // BluetoothLeBroadcastMetadata
-    static final String PREFIX_BT_ADDRESS_TYPE = "T:";
-    static final String PREFIX_BT_DEVICE = "D:";
-    static final String PREFIX_BT_ADVERTISING_SID = "AS:";
-    static final String PREFIX_BT_BROADCAST_ID = "B:";
-    static final String PREFIX_BT_SYNC_INTERVAL = "SI:";
-    static final String PREFIX_BT_IS_ENCRYPTED = "E:";
-    static final String PREFIX_BT_BROADCAST_CODE = "C:";
-    static final String PREFIX_BT_PRESENTATION_DELAY = "PD:";
-    static final String PREFIX_BT_SUBGROUPS = "SG:";
-    static final String PREFIX_BT_ANDROID_VERSION = "V:";
-
-    // BluetoothLeBroadcastSubgroup
-    static final String PREFIX_BTSG_CODEC_ID = "CID:";
-    static final String PREFIX_BTSG_CODEC_CONFIG = "CC:";
-    static final String PREFIX_BTSG_AUDIO_CONTENT = "AC:";
-    static final String PREFIX_BTSG_CHANNEL_PREF = "CP:";
-    static final String PREFIX_BTSG_BROADCAST_CHANNEL = "BC:";
-
-    // BluetoothLeAudioCodecConfigMetadata
-    static final String PREFIX_BTCC_AUDIO_LOCATION = "AL:";
-    static final String PREFIX_BTCC_RAW_METADATA = "CCRM:";
-
-    // BluetoothLeAudioContentMetadata
-    static final String PREFIX_BTAC_PROGRAM_INFO = "PI:";
-    static final String PREFIX_BTAC_LANGUAGE = "L:";
-    static final String PREFIX_BTAC_RAW_METADATA = "ACRM:";
-
-    // BluetoothLeBroadcastChannel
-    static final String PREFIX_BTBC_CHANNEL_INDEX = "CI:";
-    static final String PREFIX_BTBC_CODEC_CONFIG = "BCCM:";
-
-    static final String DELIMITER_QR_CODE = ";";
+    public static final String SCHEME_BT_BROADCAST_METADATA = "BT:BluetoothLeBroadcastMetadata:";
 }
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothLeBroadcastMetadataExt.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothLeBroadcastMetadataExt.kt
new file mode 100644
index 0000000..b54b115
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothLeBroadcastMetadataExt.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 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.settingslib.bluetooth
+
+import android.bluetooth.BluetoothLeBroadcastMetadata
+import android.os.Parcel
+import android.os.Parcelable
+import android.util.Base64
+import android.util.Log
+import com.android.settingslib.bluetooth.BluetoothBroadcastUtils.SCHEME_BT_BROADCAST_METADATA
+
+object BluetoothLeBroadcastMetadataExt {
+    private const val TAG = "BluetoothLeBroadcastMetadataExt"
+
+    /**
+     * Converts [BluetoothLeBroadcastMetadata] to QR code string.
+     *
+     * QR code string will prefix with "BT:BluetoothLeBroadcastMetadata:".
+     */
+    fun BluetoothLeBroadcastMetadata.toQrCodeString(): String =
+        SCHEME_BT_BROADCAST_METADATA + Base64.encodeToString(toBytes(this), Base64.NO_WRAP)
+
+    /**
+     * Converts QR code string to [BluetoothLeBroadcastMetadata].
+     *
+     * QR code string should prefix with "BT:BluetoothLeBroadcastMetadata:".
+     */
+    fun convertToBroadcastMetadata(qrCodeString: String): BluetoothLeBroadcastMetadata? {
+        if (!qrCodeString.startsWith(SCHEME_BT_BROADCAST_METADATA)) return null
+        return try {
+            val encodedString = qrCodeString.removePrefix(SCHEME_BT_BROADCAST_METADATA)
+            val bytes = Base64.decode(encodedString, Base64.NO_WRAP)
+            createFromBytes(BluetoothLeBroadcastMetadata.CREATOR, bytes)
+        } catch (e: Exception) {
+            Log.w(TAG, "Cannot convert QR code string to BluetoothLeBroadcastMetadata", e)
+            null
+        }
+    }
+
+    private fun toBytes(parcelable: Parcelable): ByteArray =
+        Parcel.obtain().run {
+            parcelable.writeToParcel(this, 0)
+            setDataPosition(0)
+            val bytes = marshall()
+            recycle()
+            bytes
+        }
+
+    private fun <T> createFromBytes(creator: Parcelable.Creator<T>, bytes: ByteArray): T =
+        Parcel.obtain().run {
+            unmarshall(bytes, 0, bytes.size)
+            setDataPosition(0)
+            val created = creator.createFromParcel(this)
+            recycle()
+            created
+        }
+}
\ No newline at end of file
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastMetadata.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastMetadata.java
deleted file mode 100644
index 0630a2e..0000000
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastMetadata.java
+++ /dev/null
@@ -1,454 +0,0 @@
-/*
- * Copyright (C) 2022 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.settingslib.bluetooth;
-
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.BluetoothDevice;
-import android.bluetooth.BluetoothLeAudioCodecConfigMetadata;
-import android.bluetooth.BluetoothLeAudioContentMetadata;
-import android.bluetooth.BluetoothLeBroadcastChannel;
-import android.bluetooth.BluetoothLeBroadcastMetadata;
-import android.bluetooth.BluetoothLeBroadcastSubgroup;
-import android.util.Log;
-
-import java.nio.charset.StandardCharsets;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-public class LocalBluetoothLeBroadcastMetadata {
-    private static final boolean DEBUG = BluetoothUtils.D;
-    private static final String TAG = "LocalBluetoothLeBroadcastMetadata";
-    private static final String METADATA_START = "<";
-    private static final String METADATA_END = ">";
-    private static final String PATTERN_REGEX = "<(.*?)>";
-    private static final String PATTERN_BT_BROADCAST_METADATA =
-            "T:<(.*?)>;+D:<(.*?)>;+AS:<(.*?)>;+B:<(.*?)>;+SI:<(.*?)>;+E:<(.*?)>;+C:<(.*?)>;"
-                + "+PD:<(.*?)>;+SG:(.*)";
-    private static final String PATTERN_BT_SUBGROUP =
-            "CID:<(.*?)>;+CC:<(.*?);>;+AC:<(.*?);>;+CP:<(.*?)>;+BC:<(.*)>;>;";
-    private static final String PATTERN_BT_CHANNEL = "CI:<(.*?)>;+BCCM:<(.*?);>;";
-
-    /* Index for BluetoothLeBroadcastMetadata */
-    private static int MATCH_INDEX_ADDRESS_TYPE = 1;
-    private static int MATCH_INDEX_DEVICE = 2;
-    private static int MATCH_INDEX_ADVERTISING_SID = 3;
-    private static int MATCH_INDEX_BROADCAST_ID = 4;
-    private static int MATCH_INDEX_SYNC_INTERVAL = 5;
-    private static int MATCH_INDEX_IS_ENCRYPTED = 6;
-    private static int MATCH_INDEX_BROADCAST_CODE = 7;
-    private static int MATCH_INDEX_PRESENTATION_DELAY = 8;
-    private static int MATCH_INDEX_SUBGROUPS = 9;
-
-    /* Index for BluetoothLeBroadcastSubgroup */
-    private static int MATCH_INDEX_CODEC_ID = 1;
-    private static int MATCH_INDEX_CODEC_CONFIG = 2;
-    private static int MATCH_INDEX_AUDIO_CONTENT = 3;
-    private static int MATCH_INDEX_CHANNEL_PREF = 4;
-    private static int MATCH_INDEX_BROADCAST_CHANNEL = 5;
-
-    /* Index for BluetoothLeAudioCodecConfigMetadata */
-    private static int LIST_INDEX_AUDIO_LOCATION = 0;
-    private static int LIST_INDEX_CODEC_CONFIG_RAW_METADATA = 1;
-
-    /* Index for BluetoothLeAudioContentMetadata */
-    private static int LIST_INDEX_PROGRAM_INFO = 0;
-    private static int LIST_INDEX_LANGUAGE = 1;
-    private static int LIST_INDEX_AUDIO_CONTENT_RAW_METADATA = 2;
-
-    /* Index for BluetoothLeBroadcastChannel */
-    private static int MATCH_INDEX_CHANNEL_INDEX = 1;
-    private static int MATCH_INDEX_CHANNEL_CODEC_CONFIG = 2;
-
-    private BluetoothLeBroadcastSubgroup mSubgroup;
-    private List<BluetoothLeBroadcastSubgroup> mSubgroupList;
-
-    // BluetoothLeBroadcastMetadata
-    // Optional: Identity address type
-    private int mSourceAddressType;
-    // Optional: Must use identity address
-    private BluetoothDevice mSourceDevice;
-    private int mSourceAdvertisingSid;
-    private int mBroadcastId;
-    private int mPaSyncInterval;
-    private int mPresentationDelayMicros;
-    private boolean mIsEncrypted;
-    private byte[] mBroadcastCode;
-
-    // BluetoothLeBroadcastSubgroup
-    private int mCodecId;
-    private BluetoothLeAudioContentMetadata mContentMetadata;
-    private BluetoothLeAudioCodecConfigMetadata mConfigMetadata;
-    private Boolean mNoChannelPreference;
-    private List<BluetoothLeBroadcastChannel> mChannel;
-
-    // BluetoothLeAudioCodecConfigMetadata
-    private long mAudioLocation;
-    private byte[] mCodecConfigMetadata;
-
-    // BluetoothLeAudioContentMetadata
-    private String mLanguage;
-    private String mProgramInfo;
-    private byte[] mAudioContentMetadata;
-
-    // BluetoothLeBroadcastChannel
-    private boolean mIsSelected;
-    private int mChannelIndex;
-
-
-    LocalBluetoothLeBroadcastMetadata(BluetoothLeBroadcastMetadata metadata) {
-        mSourceAddressType = metadata.getSourceAddressType();
-        mSourceDevice = metadata.getSourceDevice();
-        mSourceAdvertisingSid = metadata.getSourceAdvertisingSid();
-        mBroadcastId = metadata.getBroadcastId();
-        mPaSyncInterval = metadata.getPaSyncInterval();
-        mIsEncrypted = metadata.isEncrypted();
-        mBroadcastCode = metadata.getBroadcastCode();
-        mPresentationDelayMicros = metadata.getPresentationDelayMicros();
-        mSubgroupList = metadata.getSubgroups();
-    }
-
-    public LocalBluetoothLeBroadcastMetadata() {
-    }
-
-    public void setBroadcastCode(byte[] code) {
-        mBroadcastCode = code;
-    }
-
-    public int getBroadcastId() {
-        return mBroadcastId;
-    }
-
-    public String convertToQrCodeString() {
-        String subgroupString = convertSubgroupToString(mSubgroupList);
-        return new StringBuilder()
-                .append(BluetoothBroadcastUtils.SCHEME_BT_BROADCAST_METADATA)
-                .append(BluetoothBroadcastUtils.PREFIX_BT_ADDRESS_TYPE)
-                .append(METADATA_START).append(mSourceAddressType).append(METADATA_END)
-                .append(BluetoothBroadcastUtils.DELIMITER_QR_CODE)
-                .append(BluetoothBroadcastUtils.PREFIX_BT_DEVICE)
-                .append(METADATA_START).append(mSourceDevice).append(METADATA_END)
-                .append(BluetoothBroadcastUtils.DELIMITER_QR_CODE)
-                .append(BluetoothBroadcastUtils.PREFIX_BT_ADVERTISING_SID)
-                .append(METADATA_START).append(mSourceAdvertisingSid).append(METADATA_END)
-                .append(BluetoothBroadcastUtils.DELIMITER_QR_CODE)
-                .append(BluetoothBroadcastUtils.PREFIX_BT_BROADCAST_ID)
-                .append(METADATA_START).append(mBroadcastId).append(METADATA_END)
-                .append(BluetoothBroadcastUtils.DELIMITER_QR_CODE)
-                .append(BluetoothBroadcastUtils.PREFIX_BT_SYNC_INTERVAL)
-                .append(METADATA_START).append(mPaSyncInterval).append(METADATA_END)
-                .append(BluetoothBroadcastUtils.DELIMITER_QR_CODE)
-                .append(BluetoothBroadcastUtils.PREFIX_BT_IS_ENCRYPTED)
-                .append(METADATA_START).append(mIsEncrypted).append(METADATA_END)
-                .append(BluetoothBroadcastUtils.DELIMITER_QR_CODE)
-                .append(BluetoothBroadcastUtils.PREFIX_BT_BROADCAST_CODE)
-                .append(METADATA_START).append(Arrays.toString(mBroadcastCode)).append(METADATA_END)
-                .append(BluetoothBroadcastUtils.DELIMITER_QR_CODE)
-                .append(BluetoothBroadcastUtils.PREFIX_BT_PRESENTATION_DELAY)
-                .append(METADATA_START).append(mPresentationDelayMicros).append(METADATA_END)
-                .append(BluetoothBroadcastUtils.DELIMITER_QR_CODE)
-                .append(BluetoothBroadcastUtils.PREFIX_BT_SUBGROUPS)
-                .append(METADATA_START).append(subgroupString).append(METADATA_END)
-                .append(BluetoothBroadcastUtils.DELIMITER_QR_CODE)
-                .toString();
-    }
-
-    private String convertSubgroupToString(List<BluetoothLeBroadcastSubgroup> subgroupList) {
-        StringBuilder subgroupListBuilder = new StringBuilder();
-        String subgroupString = "";
-        for (BluetoothLeBroadcastSubgroup subgroup: subgroupList) {
-            String audioCodec = convertAudioCodecConfigToString(subgroup.getCodecSpecificConfig());
-            String audioContent = convertAudioContentToString(subgroup.getContentMetadata());
-            boolean hasChannelPreference = subgroup.hasChannelPreference();
-            String channels = convertChannelToString(subgroup.getChannels());
-            subgroupString = new StringBuilder()
-                    .append(BluetoothBroadcastUtils.PREFIX_BTSG_CODEC_ID)
-                    .append(METADATA_START).append(subgroup.getCodecId()).append(METADATA_END)
-                    .append(BluetoothBroadcastUtils.DELIMITER_QR_CODE)
-                    .append(BluetoothBroadcastUtils.PREFIX_BTSG_CODEC_CONFIG)
-                    .append(METADATA_START).append(audioCodec).append(METADATA_END)
-                    .append(BluetoothBroadcastUtils.DELIMITER_QR_CODE)
-                    .append(BluetoothBroadcastUtils.PREFIX_BTSG_AUDIO_CONTENT)
-                    .append(METADATA_START).append(audioContent).append(METADATA_END)
-                    .append(BluetoothBroadcastUtils.DELIMITER_QR_CODE)
-                    .append(BluetoothBroadcastUtils.PREFIX_BTSG_CHANNEL_PREF)
-                    .append(METADATA_START).append(hasChannelPreference).append(METADATA_END)
-                    .append(BluetoothBroadcastUtils.DELIMITER_QR_CODE)
-                    .append(BluetoothBroadcastUtils.PREFIX_BTSG_BROADCAST_CHANNEL)
-                    .append(METADATA_START).append(channels).append(METADATA_END)
-                    .append(BluetoothBroadcastUtils.DELIMITER_QR_CODE)
-                    .toString();
-            subgroupListBuilder.append(subgroupString);
-        }
-        return subgroupListBuilder.toString();
-    }
-
-    private String convertAudioCodecConfigToString(BluetoothLeAudioCodecConfigMetadata config) {
-        String audioLocation = String.valueOf(config.getAudioLocation());
-        String rawMetadata = new String(config.getRawMetadata(), StandardCharsets.UTF_8);
-        return new StringBuilder()
-            .append(BluetoothBroadcastUtils.PREFIX_BTCC_AUDIO_LOCATION)
-            .append(METADATA_START).append(audioLocation).append(METADATA_END)
-            .append(BluetoothBroadcastUtils.DELIMITER_QR_CODE)
-            .append(BluetoothBroadcastUtils.PREFIX_BTCC_RAW_METADATA)
-            .append(METADATA_START).append(rawMetadata).append(METADATA_END)
-            .append(BluetoothBroadcastUtils.DELIMITER_QR_CODE)
-            .toString();
-    }
-
-    private String convertAudioContentToString(BluetoothLeAudioContentMetadata audioContent) {
-        String rawMetadata = new String(audioContent.getRawMetadata(), StandardCharsets.UTF_8);
-        return new StringBuilder()
-            .append(BluetoothBroadcastUtils.PREFIX_BTAC_PROGRAM_INFO)
-            .append(METADATA_START).append(audioContent.getProgramInfo()).append(METADATA_END)
-            .append(BluetoothBroadcastUtils.DELIMITER_QR_CODE)
-            .append(BluetoothBroadcastUtils.PREFIX_BTAC_LANGUAGE)
-            .append(METADATA_START).append(audioContent.getLanguage()).append(METADATA_END)
-            .append(BluetoothBroadcastUtils.DELIMITER_QR_CODE)
-            .append(BluetoothBroadcastUtils.PREFIX_BTAC_RAW_METADATA)
-            .append(METADATA_START).append(rawMetadata).append(METADATA_END)
-            .append(BluetoothBroadcastUtils.DELIMITER_QR_CODE)
-            .toString();
-    }
-
-    private String convertChannelToString(List<BluetoothLeBroadcastChannel> channelList) {
-        StringBuilder channelListBuilder = new StringBuilder();
-        String channelString = "";
-        for (BluetoothLeBroadcastChannel channel: channelList) {
-            String channelAudioCodec = convertAudioCodecConfigToString(channel.getCodecMetadata());
-            channelString = new StringBuilder()
-                .append(BluetoothBroadcastUtils.PREFIX_BTBC_CHANNEL_INDEX)
-                .append(METADATA_START).append(channel.getChannelIndex()).append(METADATA_END)
-                .append(BluetoothBroadcastUtils.DELIMITER_QR_CODE)
-                .append(BluetoothBroadcastUtils.PREFIX_BTBC_CODEC_CONFIG)
-                .append(METADATA_START).append(channelAudioCodec).append(METADATA_END)
-                .append(BluetoothBroadcastUtils.DELIMITER_QR_CODE)
-                .toString();
-            channelListBuilder.append(channelString);
-        }
-        return channelListBuilder.toString();
-    }
-
-    /**
-     * Example : prefix is with the “BT:”, and end by the Android Version.
-     * BT:T:<1>;D:<00:11:22:AA:BB:CC>;AS:<1>;B:…;V:T;;
-     *
-     * @return BluetoothLeBroadcastMetadata
-     */
-    public BluetoothLeBroadcastMetadata convertToBroadcastMetadata(String qrCodeString) {
-        if (DEBUG) {
-            Log.d(TAG, "Convert " + qrCodeString + "to BluetoothLeBroadcastMetadata");
-        }
-
-        Pattern pattern = Pattern.compile(PATTERN_BT_BROADCAST_METADATA);
-        Matcher match = pattern.matcher(qrCodeString);
-        if (match.find()) {
-            try {
-                mSourceAddressType = Integer.parseInt(match.group(MATCH_INDEX_ADDRESS_TYPE));
-                mSourceDevice = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(
-                    match.group(MATCH_INDEX_DEVICE));
-                mSourceAdvertisingSid = Integer.parseInt(
-                    match.group(MATCH_INDEX_ADVERTISING_SID));
-                mBroadcastId = Integer.parseInt(match.group(MATCH_INDEX_BROADCAST_ID));
-                mPaSyncInterval = Integer.parseInt(match.group(MATCH_INDEX_SYNC_INTERVAL));
-                mIsEncrypted = Boolean.valueOf(match.group(MATCH_INDEX_IS_ENCRYPTED));
-                mBroadcastCode = match.group(MATCH_INDEX_BROADCAST_CODE).getBytes();
-                mPresentationDelayMicros =
-                    Integer.parseInt(match.group(MATCH_INDEX_PRESENTATION_DELAY));
-
-                if (DEBUG) {
-                    Log.d(TAG, "Converted qrCodeString result: "
-                        + " ,Type = " + mSourceAddressType
-                        + " ,Device = " + mSourceDevice
-                        + " ,AdSid = " + mSourceAdvertisingSid
-                        + " ,BroadcastId = " + mBroadcastId
-                        + " ,paSync = " + mPaSyncInterval
-                        + " ,encrypted = " + mIsEncrypted
-                        + " ,BroadcastCode = " + Arrays.toString(mBroadcastCode)
-                        + " ,delay = " + mPresentationDelayMicros);
-                }
-
-                mSubgroup = convertToSubgroup(match.group(MATCH_INDEX_SUBGROUPS));
-
-                return new BluetoothLeBroadcastMetadata.Builder()
-                    .setSourceDevice(mSourceDevice, mSourceAddressType)
-                    .setSourceAdvertisingSid(mSourceAdvertisingSid)
-                    .setBroadcastId(mBroadcastId)
-                    .setPaSyncInterval(mPaSyncInterval)
-                    .setEncrypted(mIsEncrypted)
-                    .setBroadcastCode(mBroadcastCode)
-                    .setPresentationDelayMicros(mPresentationDelayMicros)
-                    .addSubgroup(mSubgroup)
-                    .build();
-            } catch (IllegalArgumentException e) {
-                Log.d(TAG, "IllegalArgumentException when convert : " + e);
-                return null;
-            }
-        } else {
-            if (DEBUG) {
-                Log.d(TAG, "The match fail, can not convert it to BluetoothLeBroadcastMetadata.");
-            }
-            return null;
-        }
-    }
-
-    private BluetoothLeBroadcastSubgroup convertToSubgroup(String subgroupString) {
-        if (DEBUG) {
-            Log.d(TAG, "Convert " + subgroupString + "to BluetoothLeBroadcastSubgroup");
-        }
-        Pattern pattern = Pattern.compile(PATTERN_BT_SUBGROUP);
-        Matcher match = pattern.matcher(subgroupString);
-        if (match.find()) {
-            mCodecId = Integer.parseInt(match.group(MATCH_INDEX_CODEC_ID));
-            mConfigMetadata = convertToConfigMetadata(match.group(MATCH_INDEX_CODEC_CONFIG));
-            mContentMetadata = convertToContentMetadata(match.group(MATCH_INDEX_AUDIO_CONTENT));
-            mNoChannelPreference = Boolean.valueOf(match.group(MATCH_INDEX_CHANNEL_PREF));
-            mChannel =
-                  convertToChannel(match.group(MATCH_INDEX_BROADCAST_CHANNEL), mConfigMetadata);
-
-            BluetoothLeBroadcastSubgroup.Builder subgroupBuilder =
-                    new BluetoothLeBroadcastSubgroup.Builder();
-            subgroupBuilder.setCodecId(mCodecId);
-            subgroupBuilder.setCodecSpecificConfig(mConfigMetadata);
-            subgroupBuilder.setContentMetadata(mContentMetadata);
-
-            for (BluetoothLeBroadcastChannel channel : mChannel) {
-                subgroupBuilder.addChannel(channel);
-            }
-            return subgroupBuilder.build();
-        } else {
-            if (DEBUG) {
-                Log.d(TAG,
-                        "The match fail, can not convert it to BluetoothLeBroadcastSubgroup.");
-            }
-            return null;
-        }
-    }
-
-    private BluetoothLeAudioCodecConfigMetadata convertToConfigMetadata(
-            String configMetadataString) {
-        if (DEBUG) {
-            Log.d(TAG,
-                    "Convert " + configMetadataString + "to BluetoothLeAudioCodecConfigMetadata");
-        }
-        Pattern pattern = Pattern.compile(PATTERN_REGEX);
-        Matcher match = pattern.matcher(configMetadataString);
-        ArrayList<String> resultList = new ArrayList<>();
-        while (match.find()) {
-            resultList.add(match.group(1));
-            Log.d(TAG, "Codec Config match : " + match.group(1));
-        }
-        if (DEBUG) {
-            Log.d(TAG, "Converted configMetadataString result: " + resultList.size());
-        }
-        if (resultList.size() > 0) {
-            mAudioLocation = Long.parseLong(resultList.get(LIST_INDEX_AUDIO_LOCATION));
-            mCodecConfigMetadata = resultList.get(LIST_INDEX_CODEC_CONFIG_RAW_METADATA).getBytes();
-            return new BluetoothLeAudioCodecConfigMetadata.Builder()
-                    .setAudioLocation(mAudioLocation)
-                    .build();
-        } else {
-            if (DEBUG) {
-                Log.d(TAG,
-                        "The match fail, can not convert it to "
-                                + "BluetoothLeAudioCodecConfigMetadata.");
-            }
-            return null;
-        }
-    }
-
-    private BluetoothLeAudioContentMetadata convertToContentMetadata(String contentMetadataString) {
-        if (DEBUG) {
-            Log.d(TAG, "Convert " + contentMetadataString + "to BluetoothLeAudioContentMetadata");
-        }
-        Pattern pattern = Pattern.compile(PATTERN_REGEX);
-        Matcher match = pattern.matcher(contentMetadataString);
-        ArrayList<String> resultList = new ArrayList<>();
-        while (match.find()) {
-            Log.d(TAG, "Audio Content match : " + match.group(1));
-            resultList.add(match.group(1));
-        }
-        if (DEBUG) {
-            Log.d(TAG, "Converted contentMetadataString result: " + resultList.size());
-        }
-        if (resultList.size() > 0) {
-            mProgramInfo = resultList.get(LIST_INDEX_PROGRAM_INFO);
-            mLanguage = resultList.get(LIST_INDEX_LANGUAGE);
-            mAudioContentMetadata =
-                  resultList.get(LIST_INDEX_AUDIO_CONTENT_RAW_METADATA).getBytes();
-
-            /* TODO(b/265253566) : Need to set the default value for language when the user starts
-            *  the broadcast.
-            */
-            if (mLanguage.equals("null")) {
-                mLanguage = "eng";
-            }
-
-            return new BluetoothLeAudioContentMetadata.Builder()
-                    .setProgramInfo(mProgramInfo)
-                    .setLanguage(mLanguage)
-                    .build();
-        } else {
-            if (DEBUG) {
-                Log.d(TAG,
-                        "The match fail, can not convert it to BluetoothLeAudioContentMetadata.");
-            }
-            return null;
-        }
-    }
-
-    private List<BluetoothLeBroadcastChannel> convertToChannel(String channelString,
-            BluetoothLeAudioCodecConfigMetadata configMetadata) {
-        if (DEBUG) {
-            Log.d(TAG, "Convert " + channelString + "to BluetoothLeBroadcastChannel");
-        }
-        Pattern pattern = Pattern.compile(PATTERN_BT_CHANNEL);
-        Matcher match = pattern.matcher(channelString);
-        Map<Integer, BluetoothLeAudioCodecConfigMetadata> channel =
-                new HashMap<Integer, BluetoothLeAudioCodecConfigMetadata>();
-        while (match.find()) {
-            channel.put(Integer.parseInt(match.group(MATCH_INDEX_CHANNEL_INDEX)),
-                    convertToConfigMetadata(match.group(MATCH_INDEX_CHANNEL_CODEC_CONFIG)));
-        }
-
-        if (channel.size() > 0) {
-            mIsSelected = false;
-            ArrayList<BluetoothLeBroadcastChannel> broadcastChannelList = new ArrayList<>();
-            for (Map.Entry<Integer, BluetoothLeAudioCodecConfigMetadata> entry :
-                    channel.entrySet()) {
-
-                broadcastChannelList.add(
-                        new BluetoothLeBroadcastChannel.Builder()
-                            .setSelected(mIsSelected)
-                            .setChannelIndex(entry.getKey())
-                            .setCodecMetadata(entry.getValue())
-                            .build());
-            }
-            return broadcastChannelList;
-        } else {
-            if (DEBUG) {
-                Log.d(TAG,
-                        "The match fail, can not convert it to BluetoothLeBroadcastChannel.");
-            }
-            return null;
-        }
-    }
-}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastMetadata.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastMetadata.kt
new file mode 100644
index 0000000..870ea8d
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastMetadata.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2022 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.settingslib.bluetooth
+
+import android.bluetooth.BluetoothLeBroadcastMetadata
+import com.android.settingslib.bluetooth.BluetoothLeBroadcastMetadataExt.toQrCodeString
+
+@Deprecated("Replace with BluetoothLeBroadcastMetadataExt")
+class LocalBluetoothLeBroadcastMetadata(private val metadata: BluetoothLeBroadcastMetadata?) {
+
+    constructor() : this(null)
+
+    fun convertToQrCodeString(): String = metadata?.toQrCodeString() ?: ""
+
+    fun convertToBroadcastMetadata(qrCodeString: String) =
+        BluetoothLeBroadcastMetadataExt.convertToBroadcastMetadata(qrCodeString)
+}
\ No newline at end of file
diff --git a/packages/SettingsLib/tests/unit/Android.bp b/packages/SettingsLib/tests/unit/Android.bp
new file mode 100644
index 0000000..a4558f1
--- /dev/null
+++ b/packages/SettingsLib/tests/unit/Android.bp
@@ -0,0 +1,35 @@
+//
+// Copyright (C) 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 {
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+android_test {
+    name: "SettingsLibUnitTests",
+    test_suites: ["device-tests"],
+
+    srcs: [
+        "src/**/*.kt",
+    ],
+
+    static_libs: [
+        "SettingsLib",
+        "androidx.test.ext.junit",
+        "androidx.test.runner",
+        "truth-prebuilt",
+    ],
+}
diff --git a/packages/SettingsLib/tests/unit/AndroidManifest.xml b/packages/SettingsLib/tests/unit/AndroidManifest.xml
new file mode 100644
index 0000000..568f9cb
--- /dev/null
+++ b/packages/SettingsLib/tests/unit/AndroidManifest.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.settingslib.test">
+    <application>
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:label="Tests for SettingsLib"
+        android:targetPackage="com.android.settingslib.test">
+    </instrumentation>
+</manifest>
diff --git a/packages/SettingsLib/tests/unit/OWNERS b/packages/SettingsLib/tests/unit/OWNERS
new file mode 100644
index 0000000..66559252
--- /dev/null
+++ b/packages/SettingsLib/tests/unit/OWNERS
@@ -0,0 +1,2 @@
+# We do not guard tests - everyone is welcomed to contribute to tests.
+per-file *.kt=*
diff --git a/packages/SettingsLib/tests/unit/src/com/android/settingslib/bluetooth/BluetoothLeBroadcastMetadataExtTest.kt b/packages/SettingsLib/tests/unit/src/com/android/settingslib/bluetooth/BluetoothLeBroadcastMetadataExtTest.kt
new file mode 100644
index 0000000..0e3590d
--- /dev/null
+++ b/packages/SettingsLib/tests/unit/src/com/android/settingslib/bluetooth/BluetoothLeBroadcastMetadataExtTest.kt
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 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.settingslib.bluetooth
+
+import android.bluetooth.BluetoothAdapter
+import android.bluetooth.BluetoothDevice
+import android.bluetooth.BluetoothLeAudioCodecConfigMetadata
+import android.bluetooth.BluetoothLeAudioContentMetadata
+import android.bluetooth.BluetoothLeBroadcastChannel
+import android.bluetooth.BluetoothLeBroadcastMetadata
+import android.bluetooth.BluetoothLeBroadcastSubgroup
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settingslib.bluetooth.BluetoothLeBroadcastMetadataExt.toQrCodeString
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class BluetoothLeBroadcastMetadataExtTest {
+
+    @Test
+    fun toQrCodeString() {
+        val subgroup = BluetoothLeBroadcastSubgroup.Builder().apply {
+            setCodecId(100)
+            val audioCodecConfigMetadata = BluetoothLeAudioCodecConfigMetadata.Builder().build()
+            setCodecSpecificConfig(audioCodecConfigMetadata)
+            setContentMetadata(BluetoothLeAudioContentMetadata.Builder().build())
+            addChannel(BluetoothLeBroadcastChannel.Builder().apply {
+                setChannelIndex(1000)
+                setCodecMetadata(audioCodecConfigMetadata)
+            }.build())
+        }.build()
+
+        val metadata = BluetoothLeBroadcastMetadata.Builder().apply {
+            setSourceDevice(Device, 0)
+            setSourceAdvertisingSid(1)
+            setBroadcastId(2)
+            setPaSyncInterval(3)
+            setEncrypted(true)
+            setBroadcastCode(byteArrayOf(10, 11, 12, 13))
+            setPresentationDelayMicros(4)
+            addSubgroup(subgroup)
+        }.build()
+
+        val qrCodeString = metadata.toQrCodeString()
+
+        assertThat(qrCodeString).isEqualTo(QR_CODE_STRING)
+    }
+
+    @Test
+    fun decodeAndEncodeAgain_sameString() {
+        val metadata = BluetoothLeBroadcastMetadataExt.convertToBroadcastMetadata(QR_CODE_STRING)!!
+
+        val qrCodeString = metadata.toQrCodeString()
+
+        assertThat(qrCodeString).isEqualTo(QR_CODE_STRING)
+    }
+
+    private companion object {
+        const val TEST_DEVICE_ADDRESS = "00:A1:A1:A1:A1:A1"
+
+        val Device: BluetoothDevice =
+            BluetoothAdapter.getDefaultAdapter().getRemoteDevice(TEST_DEVICE_ADDRESS)
+
+        const val QR_CODE_STRING =
+            "BT:BluetoothLeBroadcastMetadata:AAAAAAEAAAABAAAAEQAAADAAMAA6AEEAMQA6AEEAMQA6AEEAMQA6" +
+                "AEEAMQA6AEEAMQAAAAAAAAABAAAAAgAAAAMAAAABAAAABAAAAAQAAAAKCwwNBAAAAAEAAAABAAAAZAAA" +
+                "AAAAAAABAAAAAAAAAAAAAAAGAAAABgAAAAUDAAAAAAAAAAAAAAAAAAAAAAAAAQAAAP//////////AAAA" +
+                "AAAAAAABAAAAAQAAAAAAAADoAwAAAQAAAAAAAAAAAAAABgAAAAYAAAAFAwAAAAAAAAAAAAAAAAAAAAAA" +
+                "AAAAAAD/////AAAAAAAAAAA="
+    }
+}
\ No newline at end of file