[Audiosharing] Implement name and password row.

Bug: 308368124
Test: manual
Change-Id: I86d0e771ece0ea7003a50ee0cc9305814d85fecb
diff --git a/res/xml/bluetooth_audio_sharing.xml b/res/xml/bluetooth_audio_sharing.xml
index d3aad22..5de8dfc 100644
--- a/res/xml/bluetooth_audio_sharing.xml
+++ b/res/xml/bluetooth_audio_sharing.xml
@@ -45,9 +45,15 @@
 
         <com.android.settings.connecteddevice.audiosharing.AudioSharingNamePreference
             android:key="audio_sharing_stream_name"
-            android:summary="********"
-            android:title="Stream name"
+            android:title="Name"
             settings:controller="com.android.settings.connecteddevice.audiosharing.AudioSharingNamePreferenceController" />
+
+        <com.android.settings.widget.ValidatedEditTextPreference
+            android:key="audio_sharing_stream_password"
+            android:summary="********"
+            android:title="Password"
+            settings:controller="com.android.settings.connecteddevice.audiosharing.AudioSharingPasswordPreferenceController" />
+
         <SwitchPreferenceCompat
             android:key="audio_sharing_stream_compatibility"
             android:title="Improve compatibility"
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNamePreference.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNamePreference.java
index 81465ed..44c947d 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNamePreference.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNamePreference.java
@@ -19,6 +19,8 @@
 import android.app.settings.SettingsEnums;
 import android.content.Context;
 import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
 import android.widget.ImageButton;
 
 import androidx.preference.PreferenceViewHolder;
@@ -30,6 +32,7 @@
 
 public class AudioSharingNamePreference extends ValidatedEditTextPreference {
     private static final String TAG = "AudioSharingNamePreference";
+    private boolean mShowQrCodeIcon = false;
 
     public AudioSharingNamePreference(
             Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
@@ -58,17 +61,50 @@
         setWidgetLayoutResource(R.layout.preference_widget_qrcode);
     }
 
+    void setShowQrCodeIcon(boolean show) {
+        mShowQrCodeIcon = show;
+        notifyChanged();
+    }
+
     @Override
     public void onBindViewHolder(PreferenceViewHolder holder) {
         super.onBindViewHolder(holder);
-        final ImageButton shareButton = (ImageButton) holder.findViewById(R.id.button_icon);
+
+        ImageButton shareButton = (ImageButton) holder.findViewById(R.id.button_icon);
+        View divider =
+                holder.findViewById(
+                        com.android.settingslib.widget.preference.twotarget.R.id
+                                .two_target_divider);
+
+        if (shareButton != null && divider != null) {
+            if (mShowQrCodeIcon) {
+                configureVisibleStateForQrCodeIcon(shareButton, divider);
+            } else {
+                configureInvisibleStateForQrCodeIcon(shareButton, divider);
+            }
+        } else {
+            Log.w(TAG, "onBindViewHolder() : shareButton or divider is null!");
+        }
+    }
+
+    private void configureVisibleStateForQrCodeIcon(ImageButton shareButton, View divider) {
+        divider.setVisibility(View.VISIBLE);
+        shareButton.setVisibility(View.VISIBLE);
         shareButton.setImageDrawable(getContext().getDrawable(R.drawable.ic_qrcode_24dp));
-        shareButton.setOnClickListener(
-                unused ->
-                        new SubSettingLauncher(getContext())
-                                .setTitleText("Audio sharing QR code")
-                                .setDestination(AudioStreamsQrCodeFragment.class.getName())
-                                .setSourceMetricsCategory(SettingsEnums.AUDIO_SHARING_SETTINGS)
-                                .launch());
+        shareButton.setOnClickListener(unused -> launchAudioSharingQrCodeFragment());
+    }
+
+    private void configureInvisibleStateForQrCodeIcon(ImageButton shareButton, View divider) {
+        divider.setVisibility(View.INVISIBLE);
+        shareButton.setVisibility(View.INVISIBLE);
+        shareButton.setOnClickListener(null);
+    }
+
+    private void launchAudioSharingQrCodeFragment() {
+        new SubSettingLauncher(getContext())
+                .setTitleText("Audio sharing QR code")
+                .setDestination(AudioStreamsQrCodeFragment.class.getName())
+                .setSourceMetricsCategory(SettingsEnums.AUDIO_SHARING_SETTINGS)
+                .launch();
     }
 }
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNamePreferenceController.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNamePreferenceController.java
index a3eb188..644e05e 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNamePreferenceController.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNamePreferenceController.java
@@ -16,25 +16,128 @@
 
 package com.android.settings.connecteddevice.audiosharing;
 
+import static com.android.settings.connecteddevice.audiosharing.AudioSharingUtils.isBroadcasting;
+
+import android.bluetooth.BluetoothLeBroadcast;
+import android.bluetooth.BluetoothLeBroadcastMetadata;
 import android.content.Context;
+import android.util.Log;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.lifecycle.DefaultLifecycleObserver;
+import androidx.lifecycle.LifecycleOwner;
 import androidx.preference.Preference;
+import androidx.preference.PreferenceScreen;
 
+import com.android.settings.bluetooth.Utils;
 import com.android.settings.core.BasePreferenceController;
 import com.android.settings.widget.ValidatedEditTextPreference;
+import com.android.settingslib.bluetooth.BluetoothUtils;
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.settingslib.utils.ThreadUtils;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
 
 public class AudioSharingNamePreferenceController extends BasePreferenceController
-        implements ValidatedEditTextPreference.Validator, Preference.OnPreferenceChangeListener {
+        implements ValidatedEditTextPreference.Validator,
+                Preference.OnPreferenceChangeListener,
+                DefaultLifecycleObserver {
 
     private static final String TAG = "AudioSharingNamePreferenceController";
-
+    private static final boolean DEBUG = BluetoothUtils.D;
     private static final String PREF_KEY = "audio_sharing_stream_name";
 
-    private AudioSharingNameTextValidator mAudioSharingNameTextValidator;
+    private final BluetoothLeBroadcast.Callback mBroadcastCallback =
+            new BluetoothLeBroadcast.Callback() {
+                @Override
+                public void onBroadcastMetadataChanged(
+                        int broadcastId, BluetoothLeBroadcastMetadata metadata) {
+                    if (DEBUG) {
+                        Log.d(
+                                TAG,
+                                "onBroadcastMetadataChanged() broadcastId : "
+                                        + broadcastId
+                                        + " metadata: "
+                                        + metadata);
+                    }
+                    updateQrCodeIcon(true);
+                }
+
+                @Override
+                public void onBroadcastStartFailed(int reason) {}
+
+                @Override
+                public void onBroadcastStarted(int reason, int broadcastId) {}
+
+                @Override
+                public void onBroadcastStopFailed(int reason) {}
+
+                @Override
+                public void onBroadcastStopped(int reason, int broadcastId) {
+                    if (DEBUG) {
+                        Log.d(
+                                TAG,
+                                "onBroadcastStopped() reason : "
+                                        + reason
+                                        + " broadcastId: "
+                                        + broadcastId);
+                    }
+                    updateQrCodeIcon(false);
+                }
+
+                @Override
+                public void onBroadcastUpdateFailed(int reason, int broadcastId) {
+                    Log.w(TAG, "onBroadcastUpdateFailed() reason : " + reason);
+                    // Do nothing if update failed.
+                }
+
+                @Override
+                public void onBroadcastUpdated(int reason, int broadcastId) {
+                    if (DEBUG) {
+                        Log.d(TAG, "onBroadcastUpdated() reason : " + reason);
+                    }
+                    updateBroadcastName();
+                }
+
+                @Override
+                public void onPlaybackStarted(int reason, int broadcastId) {}
+
+                @Override
+                public void onPlaybackStopped(int reason, int broadcastId) {}
+            };
+
+    @Nullable private final LocalBluetoothManager mLocalBtManager;
+    @Nullable private final LocalBluetoothLeBroadcast mBroadcast;
+    private final Executor mExecutor;
+    private final AudioSharingNameTextValidator mAudioSharingNameTextValidator;
+    @Nullable private AudioSharingNamePreference mPreference;
 
     public AudioSharingNamePreferenceController(Context context, String preferenceKey) {
         super(context, preferenceKey);
+        mLocalBtManager = Utils.getLocalBluetoothManager(context);
+        mBroadcast =
+                (mLocalBtManager != null)
+                        ? mLocalBtManager.getProfileManager().getLeAudioBroadcastProfile()
+                        : null;
         mAudioSharingNameTextValidator = new AudioSharingNameTextValidator();
+        mExecutor = Executors.newSingleThreadExecutor();
+    }
+
+    @Override
+    public void onStart(@NonNull LifecycleOwner owner) {
+        if (mBroadcast != null) {
+            mBroadcast.registerServiceCallBack(mExecutor, mBroadcastCallback);
+        }
+    }
+
+    @Override
+    public void onStop(@NonNull LifecycleOwner owner) {
+        if (mBroadcast != null) {
+            mBroadcast.unregisterServiceCallBack(mBroadcastCallback);
+        }
     }
 
     @Override
@@ -43,16 +146,76 @@
     }
 
     @Override
+    public void displayPreference(PreferenceScreen screen) {
+        super.displayPreference(screen);
+        mPreference = screen.findPreference(getPreferenceKey());
+        if (mPreference != null) {
+            mPreference.setValidator(this);
+            updateBroadcastName();
+            updateQrCodeIcon(isBroadcasting(mLocalBtManager));
+        }
+    }
+
+    @Override
     public String getPreferenceKey() {
         return PREF_KEY;
     }
 
     @Override
     public boolean onPreferenceChange(Preference preference, Object newValue) {
-        // TODO: update broadcast when name is changed.
+        if (mPreference != null
+                && mPreference.getSummary() != null
+                && ((String) newValue).contentEquals(mPreference.getSummary())) {
+            return false;
+        }
+
+        var unused =
+                ThreadUtils.postOnBackgroundThread(
+                        () -> {
+                            if (mBroadcast != null) {
+                                mBroadcast.setProgramInfo((String) newValue);
+                                if (isBroadcasting(mLocalBtManager)) {
+                                    // Update broadcast, UI update will be handled after callback
+                                    mBroadcast.updateBroadcast();
+                                } else {
+                                    // Directly update UI if no ongoing broadcast
+                                    updateBroadcastName();
+                                }
+                            }
+                        });
         return true;
     }
 
+    private void updateBroadcastName() {
+        if (mPreference != null) {
+            var unused =
+                    ThreadUtils.postOnBackgroundThread(
+                            () -> {
+                                if (mBroadcast != null) {
+                                    String name = mBroadcast.getProgramInfo();
+                                    ThreadUtils.postOnMainThread(
+                                            () -> {
+                                                if (mPreference != null) {
+                                                    mPreference.setText(name);
+                                                    mPreference.setSummary(name);
+                                                }
+                                            });
+                                }
+                            });
+        }
+    }
+
+    private void updateQrCodeIcon(boolean show) {
+        if (mPreference != null) {
+            ThreadUtils.postOnMainThread(
+                    () -> {
+                        if (mPreference != null) {
+                            mPreference.setShowQrCodeIcon(show);
+                        }
+                    });
+        }
+    }
+
     @Override
     public boolean isTextValid(String value) {
         return mAudioSharingNameTextValidator.isTextValid(value);
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNameTextValidator.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNameTextValidator.java
index 9492961..2022eb2 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNameTextValidator.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNameTextValidator.java
@@ -18,10 +18,27 @@
 
 import com.android.settings.widget.ValidatedEditTextPreference;
 
+import java.nio.charset.StandardCharsets;
+
+/**
+ * Validator for Audio Sharing Name, which should be a UTF-8 encoded string containing a minimum of
+ * 4 characters and a maximum of 32 human-readable characters.
+ */
 public class AudioSharingNameTextValidator implements ValidatedEditTextPreference.Validator {
+    private static final int MIN_LENGTH = 4;
+    private static final int MAX_LENGTH = 32;
+
     @Override
     public boolean isTextValid(String value) {
-        // TODO: Add validate rule if applicable.
-        return true;
+        if (value == null || value.length() < MIN_LENGTH || value.length() > MAX_LENGTH) {
+            return false;
+        }
+        return isValidUTF8(value);
+    }
+
+    private static boolean isValidUTF8(String value) {
+        byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
+        String reconstructedString = new String(bytes, StandardCharsets.UTF_8);
+        return value.equals(reconstructedString);
     }
 }
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPasswordPreferenceController.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPasswordPreferenceController.java
new file mode 100644
index 0000000..da0eb2e
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPasswordPreferenceController.java
@@ -0,0 +1,130 @@
+/*
+ * 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.settings.connecteddevice.audiosharing;
+
+import android.bluetooth.BluetoothLeBroadcast;
+import android.bluetooth.BluetoothLeBroadcastMetadata;
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.lifecycle.DefaultLifecycleObserver;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceScreen;
+
+import com.android.settings.bluetooth.Utils;
+import com.android.settings.core.BasePreferenceController;
+import com.android.settings.widget.ValidatedEditTextPreference;
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+public class AudioSharingPasswordPreferenceController extends BasePreferenceController
+        implements ValidatedEditTextPreference.Validator,
+                Preference.OnPreferenceChangeListener,
+                DefaultLifecycleObserver {
+    private static final String PREF_KEY = "audio_sharing_stream_password";
+
+    private final BluetoothLeBroadcast.Callback mBroadcastCallback =
+            new BluetoothLeBroadcast.Callback() {
+                @Override
+                public void onBroadcastMetadataChanged(
+                        int broadcastId, BluetoothLeBroadcastMetadata metadata) {}
+
+                @Override
+                public void onBroadcastStartFailed(int reason) {}
+
+                @Override
+                public void onBroadcastStarted(int reason, int broadcastId) {}
+
+                @Override
+                public void onBroadcastStopFailed(int reason) {}
+
+                @Override
+                public void onBroadcastStopped(int reason, int broadcastId) {}
+
+                @Override
+                public void onBroadcastUpdateFailed(int reason, int broadcastId) {}
+
+                @Override
+                public void onBroadcastUpdated(int reason, int broadcastId) {}
+
+                @Override
+                public void onPlaybackStarted(int reason, int broadcastId) {}
+
+                @Override
+                public void onPlaybackStopped(int reason, int broadcastId) {}
+            };
+    @Nullable private final LocalBluetoothLeBroadcast mBroadcast;
+    private final Executor mExecutor;
+    private final AudioSharingPasswordValidator mAudioSharingPasswordValidator;
+    @Nullable private ValidatedEditTextPreference mPreference;
+
+    public AudioSharingPasswordPreferenceController(Context context, String preferenceKey) {
+        super(context, preferenceKey);
+        mBroadcast =
+                Utils.getLocalBtManager(context).getProfileManager().getLeAudioBroadcastProfile();
+        mAudioSharingPasswordValidator = new AudioSharingPasswordValidator();
+        mExecutor = Executors.newSingleThreadExecutor();
+    }
+
+    @Override
+    public void onStart(@NonNull LifecycleOwner owner) {
+        if (mBroadcast != null) {
+            mBroadcast.registerServiceCallBack(mExecutor, mBroadcastCallback);
+        }
+    }
+
+    @Override
+    public void onStop(@NonNull LifecycleOwner owner) {
+        if (mBroadcast != null) {
+            mBroadcast.unregisterServiceCallBack(mBroadcastCallback);
+        }
+    }
+
+    @Override
+    public int getAvailabilityStatus() {
+        return AudioSharingUtils.isFeatureEnabled() ? AVAILABLE : UNSUPPORTED_ON_DEVICE;
+    }
+
+    @Override
+    public void displayPreference(PreferenceScreen screen) {
+        super.displayPreference(screen);
+        mPreference = screen.findPreference(getPreferenceKey());
+        if (mPreference != null) {
+            mPreference.setValidator(this);
+        }
+    }
+
+    @Override
+    public String getPreferenceKey() {
+        return PREF_KEY;
+    }
+
+    @Override
+    public boolean onPreferenceChange(Preference preference, Object newValue) {
+        // TODO(chelseahao): implement
+        return true;
+    }
+
+    @Override
+    public boolean isTextValid(String value) {
+        return mAudioSharingPasswordValidator.isTextValid(value);
+    }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPasswordValidator.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPasswordValidator.java
new file mode 100644
index 0000000..dbb40ec
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPasswordValidator.java
@@ -0,0 +1,51 @@
+/*
+ * 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.settings.connecteddevice.audiosharing;
+
+import com.android.settings.widget.ValidatedEditTextPreference;
+
+import java.nio.charset.StandardCharsets;
+
+/**
+ * Validator for Audio Sharing Password, which should be a UTF-8 string that has at least 4 octets
+ * and should not exceed 16 octets.
+ */
+public class AudioSharingPasswordValidator implements ValidatedEditTextPreference.Validator {
+    private static final int MIN_OCTETS = 4;
+    private static final int MAX_OCTETS = 16;
+
+    @Override
+    public boolean isTextValid(String value) {
+        if (value == null
+                || getOctetsCount(value) < MIN_OCTETS
+                || getOctetsCount(value) > MAX_OCTETS) {
+            return false;
+        }
+
+        return isValidUTF8(value);
+    }
+
+    private static int getOctetsCount(String value) {
+        return value.getBytes(StandardCharsets.UTF_8).length;
+    }
+
+    private static boolean isValidUTF8(String value) {
+        byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
+        String reconstructedString = new String(bytes, StandardCharsets.UTF_8);
+        return value.equals(reconstructedString);
+    }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingUtils.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingUtils.java
index 924b04d..242ce20 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingUtils.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingUtils.java
@@ -336,7 +336,7 @@
     }
 
     /** Returns if the broadcast is on-going. */
-    public static boolean isBroadcasting(LocalBluetoothManager manager) {
+    public static boolean isBroadcasting(@Nullable LocalBluetoothManager manager) {
         if (manager == null) return false;
         LocalBluetoothLeBroadcast broadcast =
                 manager.getProfileManager().getLeAudioBroadcastProfile();