[LE broadcast sink] Add the source list in boradcast sink UI.

Add the source list in boradcast sink UI.
Add the password dialog.

hsv link1: https://hsv.googleplex.com/6256032201310208
hsv link2: https://hsv.googleplex.com/5934966820044800
hsv link3: https://hsv.googleplex.com/6238095344140288
Bug: 228258236
Test: manual test

Change-Id: I698c2f7aba9baa9f143a98629b8796eda57fb379
diff --git a/res/drawable/bluetooth_broadcast_dialog_done.xml b/res/drawable/bluetooth_broadcast_dialog_done.xml
new file mode 100644
index 0000000..b2a5cc6
--- /dev/null
+++ b/res/drawable/bluetooth_broadcast_dialog_done.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M9.55,18 L3.85,12.3 5.275,10.875 9.55,15.15 18.725,5.975 20.15,7.4Z"/>
+</vector>
diff --git a/res/layout/bluetooth_find_broadcast_password_dialog.xml b/res/layout/bluetooth_find_broadcast_password_dialog.xml
new file mode 100644
index 0000000..f9df3f5
--- /dev/null
+++ b/res/layout/bluetooth_find_broadcast_password_dialog.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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.
+  -->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:paddingLeft="?android:attr/dialogPreferredPadding"
+    android:paddingRight="?android:attr/dialogPreferredPadding"
+    android:orientation="vertical">
+    <TextView
+        android:id="@+id/broadcast_name_text"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:minHeight="48dp"
+        android:textAlignment="viewStart"/>
+    <EditText
+        android:id="@+id/broadcast_edit_text"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:minHeight="48dp"
+        android:textAlignment="viewStart"/>
+    <TextView
+        android:id="@+id/broadcast_error_message"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="6dp"
+        style="@style/TextAppearance.ErrorText"
+        android:visibility="invisible"/>
+</LinearLayout>
\ No newline at end of file
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 63db309..27e449f 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -14139,4 +14139,10 @@
     <string name="bluetooth_find_broadcast_button_leave">Leave broadcast</string>
     <!-- The Button of the action to scan QR code [CHAR LIMIT=none] -->
     <string name="bluetooth_find_broadcast_button_scan">Scan QR code</string>
+    <!-- The title of enter password dialog in bluetooth find broadcast page. [CHAR LIMIT=none] -->
+    <string name="find_broadcast_password_dialog_title">Enter password</string>
+    <!-- The error message of enter password dialog in bluetooth find broadcast page [CHAR LIMIT=none] -->
+    <string name="find_broadcast_password_dialog_connection_error">Can\u2019t connect. Try again.</string>
+    <!-- The error message of enter password dialog in bluetooth find broadcast page [CHAR LIMIT=none] -->
+    <string name="find_broadcast_password_dialog_password_error">Wrong password</string>
 </resources>
diff --git a/src/com/android/settings/bluetooth/BluetoothBroadcastSourcePreference.java b/src/com/android/settings/bluetooth/BluetoothBroadcastSourcePreference.java
new file mode 100644
index 0000000..17b604c
--- /dev/null
+++ b/src/com/android/settings/bluetooth/BluetoothBroadcastSourcePreference.java
@@ -0,0 +1,141 @@
+/*
+ * 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.settings.bluetooth;
+
+import android.bluetooth.BluetoothLeBroadcastMetadata;
+import android.bluetooth.BluetoothLeBroadcastSubgroup;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.text.TextUtils;
+import android.view.View;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+
+import androidx.annotation.NonNull;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceViewHolder;
+
+import com.android.settings.R;
+import com.android.settingslib.Utils;
+
+import java.util.List;
+
+/**
+ * Preference to display a broadcast source in the Broadcast Source List.
+ */
+class BluetoothBroadcastSourcePreference extends Preference {
+
+    private static final int RESOURCE_ID_UNKNOWN_PROGRAM_INFO = R.string.device_info_default;
+    private static final int RESOURCE_ID_ICON = R.drawable.settings_input_antenna;
+
+    private BluetoothLeBroadcastMetadata mBluetoothLeBroadcastMetadata;
+    private ImageView mFrictionImageView;
+    private String mTitle;
+    private boolean mStatus;
+    private boolean mIsEncrypted;
+
+    BluetoothBroadcastSourcePreference(@NonNull Context context,
+            @NonNull BluetoothLeBroadcastMetadata source) {
+        super(context);
+        initUi();
+        updateMetadataAndRefreshUi(source, false);
+    }
+
+    @Override
+    public void onBindViewHolder(final PreferenceViewHolder view) {
+        super.onBindViewHolder(view);
+        view.findViewById(R.id.two_target_divider).setVisibility(View.INVISIBLE);
+        final ImageButton imageButton = (ImageButton) view.findViewById(R.id.icon_button);
+        imageButton.setVisibility(View.GONE);
+        mFrictionImageView = (ImageView) view.findViewById(R.id.friction_icon);
+        updateStatusButton();
+    }
+
+    private void initUi() {
+        setLayoutResource(R.layout.preference_access_point);
+        setWidgetLayoutResource(R.layout.access_point_friction_widget);
+
+        mStatus = false;
+        final Drawable drawable = getContext().getDrawable(RESOURCE_ID_ICON);
+        if (drawable != null) {
+            drawable.setTint(Utils.getColorAttrDefaultColor(getContext(),
+                    android.R.attr.colorControlNormal));
+            setIcon(drawable);
+        }
+    }
+
+    private void updateStatusButton() {
+        if (mFrictionImageView == null) {
+            return;
+        }
+        if (mStatus || mIsEncrypted) {
+            Drawable drawable;
+            if (mStatus) {
+                drawable = getContext().getDrawable(R.drawable.bluetooth_broadcast_dialog_done);
+            } else {
+                drawable = getContext().getDrawable(R.drawable.ic_friction_lock_closed);
+            }
+            if (drawable != null) {
+                drawable.setTint(Utils.getColorAttrDefaultColor(getContext(),
+                        android.R.attr.colorControlNormal));
+                mFrictionImageView.setImageDrawable(drawable);
+            }
+            mFrictionImageView.setVisibility(View.VISIBLE);
+        } else {
+            mFrictionImageView.setVisibility(View.GONE);
+        }
+    }
+
+    /**
+     * Updates the title and status from BluetoothLeBroadcastMetadata.
+     */
+    public void updateMetadataAndRefreshUi(BluetoothLeBroadcastMetadata source, boolean status) {
+        mBluetoothLeBroadcastMetadata = source;
+        mTitle = getBroadcastMetadataProgramInfo();
+        mIsEncrypted = mBluetoothLeBroadcastMetadata.isEncrypted();
+        mStatus = status;
+
+        refresh();
+    }
+
+    /**
+     * Gets the BluetoothLeBroadcastMetadata.
+     */
+    public BluetoothLeBroadcastMetadata getBluetoothLeBroadcastMetadata() {
+        return mBluetoothLeBroadcastMetadata;
+    }
+
+    private void refresh() {
+        setTitle(mTitle);
+        updateStatusButton();
+    }
+
+    private String getBroadcastMetadataProgramInfo() {
+        if (mBluetoothLeBroadcastMetadata == null) {
+            return getContext().getString(RESOURCE_ID_UNKNOWN_PROGRAM_INFO);
+        }
+        final List<BluetoothLeBroadcastSubgroup> subgroups =
+                mBluetoothLeBroadcastMetadata.getSubgroups();
+        if (subgroups.isEmpty()) {
+            return getContext().getString(RESOURCE_ID_UNKNOWN_PROGRAM_INFO);
+        }
+        return subgroups.stream()
+                .map(i -> i.getContentMetadata().getProgramInfo())
+                .filter(i -> !TextUtils.isEmpty(i))
+                .findFirst().orElse(getContext().getString(RESOURCE_ID_UNKNOWN_PROGRAM_INFO));
+    }
+}
diff --git a/src/com/android/settings/bluetooth/BluetoothFindBroadcastsFragment.java b/src/com/android/settings/bluetooth/BluetoothFindBroadcastsFragment.java
index f251db5..9a26a57 100644
--- a/src/com/android/settings/bluetooth/BluetoothFindBroadcastsFragment.java
+++ b/src/com/android/settings/bluetooth/BluetoothFindBroadcastsFragment.java
@@ -19,33 +19,53 @@
 import static android.bluetooth.BluetoothDevice.BOND_NONE;
 import static android.os.UserManager.DISALLOW_CONFIG_BLUETOOTH;
 
+import android.app.AlertDialog;
 import android.app.settings.SettingsEnums;
 import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothLeBroadcastAssistant;
+import android.bluetooth.BluetoothLeBroadcastMetadata;
+import android.bluetooth.BluetoothLeBroadcastReceiveState;
+import android.bluetooth.le.ScanFilter;
 import android.content.Context;
+import android.os.Bundle;
 import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.WindowManager;
+import android.widget.EditText;
+import android.widget.TextView;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.VisibleForTesting;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceCategory;
 
 import com.android.settings.R;
 import com.android.settings.dashboard.RestrictedDashboardFragment;
 import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
 import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
 import com.android.settingslib.core.AbstractPreferenceController;
 import com.android.settingslib.core.lifecycle.Lifecycle;
 
+import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
 
 /**
  * This fragment allowed users to find the nearby broadcast sources.
  */
 public class BluetoothFindBroadcastsFragment extends RestrictedDashboardFragment {
 
-    private static final String TAG = "BTFindBroadcastsFrg";
+    private static final String TAG = "BtFindBroadcastsFrg";
 
     public static final String KEY_DEVICE_ADDRESS = "device_address";
-
-    public static final String PREF_KEY_BROADCAST_SOURCE = "broadcast_source";
+    public static final String PREF_KEY_BROADCAST_SOURCE_LIST = "broadcast_source_list";
 
     @VisibleForTesting
     String mDeviceAddress;
@@ -53,6 +73,91 @@
     LocalBluetoothManager mManager;
     @VisibleForTesting
     CachedBluetoothDevice mCachedDevice;
+    @VisibleForTesting
+    PreferenceCategory mBroadcastSourceListCategory;
+    private LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant;
+    private BluetoothBroadcastSourcePreference mSelectedPreference;
+    private Executor mExecutor;
+    private int mSourceId;
+
+    private BluetoothLeBroadcastAssistant.Callback mBroadcastAssistantCallback =
+            new BluetoothLeBroadcastAssistant.Callback() {
+                @Override
+                public void onSearchStarted(int reason) {
+                    Log.d(TAG, "onSearchStarted: " + reason);
+
+                    getActivity().runOnUiThread(
+                            () -> cacheRemoveAllPrefs(mBroadcastSourceListCategory));
+                }
+
+                @Override
+                public void onSearchStartFailed(int reason) {
+                    Log.d(TAG, "onSearchStartFailed: " + reason);
+
+                }
+
+                @Override
+                public void onSearchStopped(int reason) {
+                    Log.d(TAG, "onSearchStopped: " + reason);
+                }
+
+                @Override
+                public void onSearchStopFailed(int reason) {
+                    Log.d(TAG, "onSearchStopFailed: " + reason);
+                }
+
+                @Override
+                public void onSourceFound(@NonNull BluetoothLeBroadcastMetadata source) {
+                    Log.d(TAG, "onSourceFound:");
+                    getActivity().runOnUiThread(() -> updateListCategory(source, false));
+                }
+
+                @Override
+                public void onSourceAdded(@NonNull BluetoothDevice sink, int sourceId, int reason) {
+                    setSourceId(sourceId);
+                    if (mSelectedPreference == null) {
+                        Log.w(TAG, "onSourceAdded: mSelectedPreference == null!");
+                        return;
+                    }
+                    getActivity().runOnUiThread(() -> updateListCategory(
+                            mSelectedPreference.getBluetoothLeBroadcastMetadata(), true));
+                }
+
+                @Override
+                public void onSourceAddFailed(@NonNull BluetoothDevice sink,
+                        @NonNull BluetoothLeBroadcastMetadata source, int reason) {
+                    mSelectedPreference = null;
+                    Log.d(TAG, "onSourceAddFailed: clear the mSelectedPreference.");
+                }
+
+                @Override
+                public void onSourceModified(@NonNull BluetoothDevice sink, int sourceId,
+                        int reason) {
+                }
+
+                @Override
+                public void onSourceModifyFailed(@NonNull BluetoothDevice sink, int sourceId,
+                        int reason) {
+                }
+
+                @Override
+                public void onSourceRemoved(@NonNull BluetoothDevice sink, int sourceId,
+                        int reason) {
+                    Log.d(TAG, "onSourceRemoved:");
+                }
+
+                @Override
+                public void onSourceRemoveFailed(@NonNull BluetoothDevice sink, int sourceId,
+                        int reason) {
+                    Log.d(TAG, "onSourceRemoveFailed:");
+                }
+
+                @Override
+                public void onReceiveStateChanged(@NonNull BluetoothDevice sink, int sourceId,
+                        @NonNull BluetoothLeBroadcastReceiveState state) {
+                    Log.d(TAG, "onReceiveStateChanged:");
+                }
+            };
 
     public BluetoothFindBroadcastsFragment() {
         super(DISALLOW_CONFIG_BLUETOOTH);
@@ -75,19 +180,50 @@
         mDeviceAddress = getArguments().getString(KEY_DEVICE_ADDRESS);
         mManager = getLocalBluetoothManager(context);
         mCachedDevice = getCachedDevice(mDeviceAddress);
+        mLeBroadcastAssistant = getLeBroadcastAssistant();
+        mExecutor = Executors.newSingleThreadExecutor();
+
         super.onAttach(context);
-        if (mCachedDevice == null) {
+        if (mCachedDevice == null || mLeBroadcastAssistant == null) {
             //Close this page if device is null with invalid device mac address
-            Log.w(TAG, "onAttach() CachedDevice is null!");
+            //or if the device does not have LeBroadcastAssistant profile
+            Log.w(TAG, "onAttach() CachedDevice or LeBroadcastAssistant is null!");
             finish();
             return;
         }
     }
 
     @Override
+    public void onCreate(Bundle icicle) {
+        super.onCreate(icicle);
+
+        mBroadcastSourceListCategory = findPreference(PREF_KEY_BROADCAST_SOURCE_LIST);
+    }
+
+    @Override
+    public void onStart() {
+        super.onStart();
+        if (mLeBroadcastAssistant != null) {
+            mLeBroadcastAssistant.registerServiceCallBack(mExecutor, mBroadcastAssistantCallback);
+        }
+    }
+
+    @Override
     public void onResume() {
         super.onResume();
         finishFragmentIfNecessary();
+        //check assistant status. Start searching...
+        if (mLeBroadcastAssistant != null && !mLeBroadcastAssistant.isSearchInProgress()) {
+            mLeBroadcastAssistant.startSearchingForSources(getScanFilter());
+        }
+    }
+
+    @Override
+    public void onStop() {
+        super.onStop();
+        if (mLeBroadcastAssistant != null) {
+            mLeBroadcastAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback);
+        }
     }
 
     @VisibleForTesting
@@ -125,4 +261,110 @@
         }
         return controllers;
     }
+
+    private LocalBluetoothLeBroadcastAssistant getLeBroadcastAssistant() {
+        if (mManager == null) {
+            Log.w(TAG, "getLeBroadcastAssistant: LocalBluetoothManager is null!");
+            return null;
+        }
+
+        LocalBluetoothProfileManager profileManager = mManager.getProfileManager();
+        if (profileManager == null) {
+            Log.w(TAG, "getLeBroadcastAssistant: LocalBluetoothProfileManager is null!");
+            return null;
+        }
+
+        return profileManager.getLeAudioBroadcastAssistantProfile();
+    }
+
+    private List<ScanFilter> getScanFilter() {
+        // Currently there is no function for setting the ScanFilter. It may have this function
+        // in the further.
+        return Collections.emptyList();
+    }
+
+    private void updateListCategory(BluetoothLeBroadcastMetadata source, boolean isConnected) {
+        BluetoothBroadcastSourcePreference item = mBroadcastSourceListCategory.findPreference(
+                Integer.toString(source.getBroadcastId()));
+        if (item == null) {
+            item = createBluetoothBroadcastSourcePreference(source);
+            mBroadcastSourceListCategory.addPreference(item);
+        }
+        item.updateMetadataAndRefreshUi(source, isConnected);
+        item.setOrder(isConnected ? 0 : 1);
+    }
+
+    private BluetoothBroadcastSourcePreference createBluetoothBroadcastSourcePreference(
+            BluetoothLeBroadcastMetadata source) {
+        BluetoothBroadcastSourcePreference pref = new BluetoothBroadcastSourcePreference(
+                getContext(), source);
+        pref.setKey(Integer.toString(source.getBroadcastId()));
+        pref.setOnPreferenceClickListener(preference -> {
+            if (source.isEncrypted()) {
+                launchBroadcastCodeDialog(pref);
+            } else {
+                addSource(pref);
+            }
+            return true;
+        });
+        return pref;
+    }
+
+    private void addSource(BluetoothBroadcastSourcePreference pref) {
+        if (mLeBroadcastAssistant == null || mCachedDevice == null) {
+            Log.w(TAG, "addSource: LeBroadcastAssistant or CachedDevice is null!");
+            return;
+        }
+        if (mSelectedPreference != null) {
+            // The previous preference status set false after user selects the new Preference.
+            getActivity().runOnUiThread(
+                    () -> {
+                        mSelectedPreference.updateMetadataAndRefreshUi(
+                                mSelectedPreference.getBluetoothLeBroadcastMetadata(), false);
+                        mSelectedPreference.setOrder(1);
+                    });
+        }
+        mSelectedPreference = pref;
+        mLeBroadcastAssistant.addSource(mCachedDevice.getDevice(),
+                pref.getBluetoothLeBroadcastMetadata(), true);
+    }
+
+    private void addBroadcastCodeIntoPreference(BluetoothBroadcastSourcePreference pref,
+            String broadcastCode) {
+        BluetoothLeBroadcastMetadata metadata =
+                new BluetoothLeBroadcastMetadata.Builder(pref.getBluetoothLeBroadcastMetadata())
+                        .setBroadcastCode(broadcastCode.getBytes(StandardCharsets.UTF_8))
+                        .build();
+        pref.updateMetadataAndRefreshUi(metadata, false);
+    }
+
+    private void launchBroadcastCodeDialog(BluetoothBroadcastSourcePreference pref) {
+        final View layout = LayoutInflater.from(getContext()).inflate(
+                R.layout.bluetooth_find_broadcast_password_dialog, null);
+        final TextView broadcastName = layout.requireViewById(R.id.broadcast_name_text);
+        final EditText editText = layout.requireViewById(R.id.broadcast_edit_text);
+        broadcastName.setText(pref.getTitle());
+        AlertDialog alertDialog = new AlertDialog.Builder(getContext())
+                .setTitle(R.string.find_broadcast_password_dialog_title)
+                .setView(layout)
+                .setNeutralButton(android.R.string.cancel, null)
+                .setPositiveButton(R.string.bluetooth_connect_access_dialog_positive,
+                        (d, w) -> {
+                            Log.d(TAG, "setPositiveButton: clicked");
+                            addBroadcastCodeIntoPreference(pref, editText.getText().toString());
+                            addSource(pref);
+                        })
+                .create();
+
+        alertDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG);
+        alertDialog.show();
+    }
+
+    public int getSourceId() {
+        return mSourceId;
+    }
+
+    public void setSourceId(int sourceId) {
+        mSourceId = sourceId;
+    }
 }