Merge "Fix Java crash problem" into main
diff --git a/res/layout/keyboard_layout_picker.xml b/res/layout/keyboard_layout_picker.xml
index 6b163da..b25c228 100644
--- a/res/layout/keyboard_layout_picker.xml
+++ b/res/layout/keyboard_layout_picker.xml
@@ -20,6 +20,13 @@
     android:id="@+id/keyboard_layout_picker_container"
     android:orientation="vertical">
 
+    <ImageView
+        android:id="@+id/keyboard_layout_preview"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:adjustViewBounds="true"
+        android:scaleType="fitCenter"/>
+
     <FrameLayout
         android:id="@+id/keyboard_layout_title"
         android:layout_width="match_parent"
diff --git a/res/xml/audio_stream_details_fragment.xml b/res/xml/audio_stream_details_fragment.xml
index 9727442..2a84939 100644
--- a/res/xml/audio_stream_details_fragment.xml
+++ b/res/xml/audio_stream_details_fragment.xml
@@ -25,10 +25,12 @@
         android:layout="@layout/settings_entity_header"
         android:selectable="false"
         settings:allowDividerBelow="true"
-        settings:searchable="false" />
+        settings:searchable="false"
+        settings:controller="com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamHeaderController" />
 
     <com.android.settingslib.widget.ActionButtonsPreference
         android:key="audio_stream_button"
-        settings:allowDividerBelow="true" />
+        settings:allowDividerBelow="true"
+        settings:controller="com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamButtonController" />
 
 </PreferenceScreen>
diff --git a/res/xml/development_settings.xml b/res/xml/development_settings.xml
index e1ccad8..f0a2881 100644
--- a/res/xml/development_settings.xml
+++ b/res/xml/development_settings.xml
@@ -727,11 +727,10 @@
             android:title="@string/enable_non_resizable_multi_window"
             android:summary="@string/enable_non_resizable_multi_window_summary" />
 
-        // TODO(b/315859328): Temporally removed since causing search indexing failure.
-<!--        <SwitchPreferenceCompat-->
-<!--            android:key="back_navigation_animation"-->
-<!--            android:title="@string/back_navigation_animation"-->
-<!--            android:summary="@string/back_navigation_animation_summary" />-->
+        <SwitchPreferenceCompat
+            android:key="back_navigation_animation"
+            android:title="@string/back_navigation_animation"
+            android:summary="@string/back_navigation_animation_summary" />
 
         <Preference
             android:key="reset_shortcut_manager_throttling"
diff --git a/src/com/android/settings/SettingsPreferenceFragment.java b/src/com/android/settings/SettingsPreferenceFragment.java
index d7a276e..66397c0 100644
--- a/src/com/android/settings/SettingsPreferenceFragment.java
+++ b/src/com/android/settings/SettingsPreferenceFragment.java
@@ -479,7 +479,10 @@
                 mDialogFragment.dismiss();
                 mDialogFragment = null;
             }
-            getListView().clearOnScrollListeners();
+            RecyclerView view = getListView();
+            if (view != null) {
+                view.clearOnScrollListeners();
+            }
         }
         super.onDetach();
     }
diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollFindSensor.java b/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollFindSensor.java
index b71330a..059173c 100644
--- a/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollFindSensor.java
+++ b/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollFindSensor.java
@@ -290,6 +290,17 @@
     }
 
     @Override
+    protected Intent getFingerprintEnrollingIntent() {
+        final Intent ret = super.getFingerprintEnrollingIntent();
+        if (Flags.udfpsEnrollCalibration()) {
+            if (mCalibrator != null) {
+                ret.putExtras(mCalibrator.getExtrasForNextIntent(true));
+            }
+        }
+        return ret;
+    }
+
+    @Override
     public void onBackPressed() {
         stopLookingForFingerprint();
         super.onBackPressed();
diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollIntroduction.java b/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollIntroduction.java
index bd52b64..aef3c06 100644
--- a/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollIntroduction.java
+++ b/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollIntroduction.java
@@ -385,7 +385,7 @@
         }
         if (Flags.udfpsEnrollCalibration()) {
             if (mCalibrator != null) {
-                intent.putExtras(mCalibrator.getExtrasForNextIntent());
+                intent.putExtras(mCalibrator.getExtrasForNextIntent(false));
             }
         }
         return intent;
diff --git a/src/com/android/settings/biometrics/fingerprint/UdfpsEnrollCalibrator.kt b/src/com/android/settings/biometrics/fingerprint/UdfpsEnrollCalibrator.kt
index 9809bcc..c54c6b5 100644
--- a/src/com/android/settings/biometrics/fingerprint/UdfpsEnrollCalibrator.kt
+++ b/src/com/android/settings/biometrics/fingerprint/UdfpsEnrollCalibrator.kt
@@ -6,7 +6,7 @@
 
 interface UdfpsEnrollCalibrator {
 
-    val extrasForNextIntent: Bundle
+    fun getExtrasForNextIntent(isEnrolling: Boolean): Bundle
 
     fun onSaveInstanceState(outState: Bundle)
 
diff --git a/src/com/android/settings/connecteddevice/AvailableMediaDeviceGroupController.java b/src/com/android/settings/connecteddevice/AvailableMediaDeviceGroupController.java
index c095fee..fc3493c 100644
--- a/src/com/android/settings/connecteddevice/AvailableMediaDeviceGroupController.java
+++ b/src/com/android/settings/connecteddevice/AvailableMediaDeviceGroupController.java
@@ -17,11 +17,16 @@
 
 import static com.android.settingslib.Utils.isAudioModeOngoingCall;
 
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothLeBroadcastAssistant;
+import android.bluetooth.BluetoothLeBroadcastMetadata;
+import android.bluetooth.BluetoothLeBroadcastReceiveState;
 import android.bluetooth.BluetoothProfile;
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.util.Log;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.VisibleForTesting;
 import androidx.fragment.app.FragmentManager;
 import androidx.preference.Preference;
@@ -33,36 +38,94 @@
 import com.android.settings.bluetooth.AvailableMediaBluetoothDeviceUpdater;
 import com.android.settings.bluetooth.BluetoothDeviceUpdater;
 import com.android.settings.bluetooth.Utils;
+import com.android.settings.connecteddevice.audiosharing.AudioSharingUtils;
 import com.android.settings.core.BasePreferenceController;
 import com.android.settings.dashboard.DashboardFragment;
 import com.android.settingslib.bluetooth.BluetoothCallback;
+import com.android.settingslib.bluetooth.BluetoothUtils;
 import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
 import com.android.settingslib.bluetooth.LocalBluetoothManager;
 import com.android.settingslib.core.lifecycle.LifecycleObserver;
 import com.android.settingslib.core.lifecycle.events.OnStart;
 import com.android.settingslib.core.lifecycle.events.OnStop;
 
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
 /**
- * Controller to maintain the {@link androidx.preference.PreferenceGroup} for all
- * available media devices. It uses {@link DevicePreferenceCallback}
- * to add/remove {@link Preference}
+ * Controller to maintain the {@link androidx.preference.PreferenceGroup} for all available media
+ * devices. It uses {@link DevicePreferenceCallback} to add/remove {@link Preference}
  */
 public class AvailableMediaDeviceGroupController extends BasePreferenceController
         implements LifecycleObserver, OnStart, OnStop, DevicePreferenceCallback, BluetoothCallback {
+    private static final boolean DEBUG = BluetoothUtils.D;
 
     private static final String TAG = "AvailableMediaDeviceGroupController";
     private static final String KEY = "available_device_list";
 
-    @VisibleForTesting
-    PreferenceGroup mPreferenceGroup;
-    @VisibleForTesting
-    LocalBluetoothManager mLocalBluetoothManager;
+    @VisibleForTesting PreferenceGroup mPreferenceGroup;
+    @VisibleForTesting LocalBluetoothManager mLocalBluetoothManager;
+    private final Executor mExecutor;
     private BluetoothDeviceUpdater mBluetoothDeviceUpdater;
     private FragmentManager mFragmentManager;
+    private BluetoothLeBroadcastAssistant.Callback mAssistantCallback =
+            new BluetoothLeBroadcastAssistant.Callback() {
+                @Override
+                public void onSearchStarted(int reason) {}
+
+                @Override
+                public void onSearchStartFailed(int reason) {}
+
+                @Override
+                public void onSearchStopped(int reason) {}
+
+                @Override
+                public void onSearchStopFailed(int reason) {}
+
+                @Override
+                public void onSourceFound(@NonNull BluetoothLeBroadcastMetadata source) {}
+
+                @Override
+                public void onSourceAdded(@NonNull BluetoothDevice sink, int sourceId, int reason) {
+                    mBluetoothDeviceUpdater.forceUpdate();
+                }
+
+                @Override
+                public void onSourceAddFailed(
+                        @NonNull BluetoothDevice sink,
+                        @NonNull BluetoothLeBroadcastMetadata source,
+                        int reason) {}
+
+                @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) {
+                    mBluetoothDeviceUpdater.forceUpdate();
+                }
+
+                @Override
+                public void onSourceRemoveFailed(
+                        @NonNull BluetoothDevice sink, int sourceId, int reason) {}
+
+                @Override
+                public void onReceiveStateChanged(
+                        BluetoothDevice sink,
+                        int sourceId,
+                        BluetoothLeBroadcastReceiveState state) {}
+            };
 
     public AvailableMediaDeviceGroupController(Context context) {
         super(context, KEY);
         mLocalBluetoothManager = Utils.getLocalBtManager(mContext);
+        mExecutor = Executors.newSingleThreadExecutor();
     }
 
     @Override
@@ -71,6 +134,18 @@
             Log.e(TAG, "onStart() Bluetooth is not supported on this device");
             return;
         }
+        if (AudioSharingUtils.isFeatureEnabled()) {
+            LocalBluetoothLeBroadcastAssistant assistant =
+                    mLocalBluetoothManager
+                            .getProfileManager()
+                            .getLeAudioBroadcastAssistantProfile();
+            if (assistant != null) {
+                if (DEBUG) {
+                    Log.d(TAG, "onStart() Register callbacks for assistant.");
+                }
+                assistant.registerServiceCallBack(mExecutor, mAssistantCallback);
+            }
+        }
         mBluetoothDeviceUpdater.registerCallback();
         mLocalBluetoothManager.getEventManager().registerCallback(this);
         mBluetoothDeviceUpdater.refreshPreference();
@@ -82,6 +157,18 @@
             Log.e(TAG, "onStop() Bluetooth is not supported on this device");
             return;
         }
+        if (AudioSharingUtils.isFeatureEnabled()) {
+            LocalBluetoothLeBroadcastAssistant assistant =
+                    mLocalBluetoothManager
+                            .getProfileManager()
+                            .getLeAudioBroadcastAssistantProfile();
+            if (assistant != null) {
+                if (DEBUG) {
+                    Log.d(TAG, "onStop() Register callbacks for assistant.");
+                }
+                assistant.unregisterServiceCallBack(mAssistantCallback);
+            }
+        }
         mBluetoothDeviceUpdater.unregisterCallback();
         mLocalBluetoothManager.getEventManager().unregisterCallback(this);
     }
@@ -130,8 +217,11 @@
 
     public void init(DashboardFragment fragment) {
         mFragmentManager = fragment.getParentFragmentManager();
-        mBluetoothDeviceUpdater = new AvailableMediaBluetoothDeviceUpdater(fragment.getContext(),
-                AvailableMediaDeviceGroupController.this, fragment.getMetricsCategory());
+        mBluetoothDeviceUpdater =
+                new AvailableMediaBluetoothDeviceUpdater(
+                        fragment.getContext(),
+                        AvailableMediaDeviceGroupController.this,
+                        fragment.getMetricsCategory());
     }
 
     @VisibleForTesting
@@ -157,20 +247,20 @@
         }
 
         if (bluetoothProfile == BluetoothProfile.HEARING_AID) {
-            HearingAidUtils.launchHearingAidPairingDialog(mFragmentManager, activeDevice,
-                    getMetricsCategory());
+            HearingAidUtils.launchHearingAidPairingDialog(
+                    mFragmentManager, activeDevice, getMetricsCategory());
         }
     }
 
     private void updateTitle() {
         if (isAudioModeOngoingCall(mContext)) {
             // in phone call
-            mPreferenceGroup.
-                    setTitle(mContext.getString(R.string.connected_device_call_device_title));
+            mPreferenceGroup.setTitle(
+                    mContext.getString(R.string.connected_device_call_device_title));
         } else {
             // without phone call
-            mPreferenceGroup.
-                    setTitle(mContext.getString(R.string.connected_device_media_device_title));
+            mPreferenceGroup.setTitle(
+                    mContext.getString(R.string.connected_device_media_device_title));
         }
     }
 }
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDevicePreferenceController.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDevicePreferenceController.java
index 9329cc29..70859c2 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDevicePreferenceController.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDevicePreferenceController.java
@@ -47,6 +47,9 @@
 import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast;
 import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
 import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.settingslib.utils.ThreadUtils;
+
+import com.google.common.collect.ImmutableList;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -260,6 +263,9 @@
             return;
         }
         mLocalBtManager.getEventManager().registerCallback(this);
+        if (DEBUG) {
+            Log.d(TAG, "onStart() Register callbacks for broadcast and assistant.");
+        }
         mBroadcast.registerServiceCallBack(mExecutor, mBroadcastCallback);
         mAssistant.registerServiceCallBack(mExecutor, mBroadcastAssistantCallback);
         mBluetoothDeviceUpdater.registerCallback();
@@ -281,15 +287,11 @@
             return;
         }
         mLocalBtManager.getEventManager().unregisterCallback(this);
-        // TODO: verify the reason for failing to unregister
-        try {
-            mBroadcast.unregisterServiceCallBack(mBroadcastCallback);
-            mAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback);
-        } catch (IllegalArgumentException e) {
-            Log.e(
-                    TAG,
-                    "Fail to unregister broadcast or assistant callback due to " + e.getMessage());
+        if (DEBUG) {
+            Log.d(TAG, "onStop() Unregister callbacks for broadcast and assistant.");
         }
+        mBroadcast.unregisterServiceCallBack(mBroadcastCallback);
+        mAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback);
         mBluetoothDeviceUpdater.unregisterCallback();
     }
 
@@ -358,6 +360,28 @@
                     "Ignore onProfileConnectionStateChanged, no broadcast or assistant supported");
             return;
         }
+        var unused =
+                ThreadUtils.postOnBackgroundThread(
+                        () -> handleOnProfileStateChanged(cachedDevice, bluetoothProfile));
+    }
+
+    /**
+     * Initialize the controller.
+     *
+     * @param fragment The fragment to provide the context and metrics category for {@link
+     *     AudioSharingBluetoothDeviceUpdater} and provide the host for dialogs.
+     */
+    public void init(DashboardFragment fragment) {
+        mFragment = fragment;
+        mBluetoothDeviceUpdater =
+                new AudioSharingBluetoothDeviceUpdater(
+                        fragment.getContext(),
+                        AudioSharingDevicePreferenceController.this,
+                        fragment.getMetricsCategory());
+    }
+
+    private void handleOnProfileStateChanged(
+            @NonNull CachedBluetoothDevice cachedDevice, int bluetoothProfile) {
         boolean isLeAudioSupported = isLeAudioSupported(cachedDevice);
         // For eligible (LE audio) remote device, we only check its connected LE audio profile.
         if (isLeAudioSupported && bluetoothProfile != BluetoothProfile.LE_AUDIO) {
@@ -384,120 +408,143 @@
         }
         if (!isLeAudioSupported) {
             // Handle connected ineligible (non LE audio) remote device
-            if (isBroadcasting()) {
-                // Show stop audio sharing dialog when an ineligible (non LE audio) remote device
-                // connected during a sharing session.
-                closeOpeningDialogs();
-                AudioSharingStopDialogFragment.show(
-                        mFragment,
-                        cachedDevice.getName(),
-                        () -> mBroadcast.stopBroadcast(mBroadcast.getLatestBroadcastId()));
-            } else {
-                // Do nothing for ineligible (non LE audio) remote device when no sharing session.
-                if (DEBUG) {
-                    Log.d(
-                            TAG,
-                            "Ignore onProfileConnectionStateChanged for non LE audio without"
-                                + " sharing session");
-                }
-            }
+            handleOnProfileStateChangedForNonLeAudioDevice(cachedDevice);
         } else {
-            Map<Integer, List<CachedBluetoothDevice>> groupedDevices =
-                    AudioSharingUtils.fetchConnectedDevicesByGroupId(mLocalBtManager);
             // Handle connected eligible (LE audio) remote device
-            if (isBroadcasting()) {
-                // Show audio sharing switch or join dialog according to device count in the sharing
-                // session.
-                ArrayList<AudioSharingDeviceItem> deviceItemsInSharingSession =
-                        AudioSharingUtils.buildOrderedConnectedLeadAudioSharingDeviceItem(
-                                mLocalBtManager, groupedDevices, /* filterByInSharing= */ true);
-                // Show audio sharing switch dialog when the third eligible (LE audio) remote device
-                // connected during a sharing session.
-                if (deviceItemsInSharingSession.size() >= 2) {
-                    closeOpeningDialogs();
-                    AudioSharingDisconnectDialogFragment.show(
-                            mFragment,
-                            deviceItemsInSharingSession,
-                            cachedDevice.getName(),
-                            (AudioSharingDeviceItem item) -> {
-                                // Remove all sources from the device user clicked
-                                for (CachedBluetoothDevice device :
-                                        groupedDevices.get(item.getGroupId())) {
-                                    for (BluetoothLeBroadcastReceiveState source :
-                                            mAssistant.getAllSources(device.getDevice())) {
-                                        mAssistant.removeSource(
-                                                device.getDevice(), source.getSourceId());
-                                    }
-                                }
-                                // Add current broadcast to the latest connected device
-                                mAssistant.addSource(
-                                        cachedDevice.getDevice(),
-                                        mBroadcast.getLatestBluetoothLeBroadcastMetadata(),
-                                        /* isGroupOp= */ true);
-                            });
-                } else {
-                    // Show audio sharing join dialog when the first or second eligible (LE audio)
-                    // remote device connected during a sharing session.
-                    closeOpeningDialogs();
-                    AudioSharingJoinDialogFragment.show(
-                            mFragment,
-                            deviceItemsInSharingSession,
-                            cachedDevice.getName(),
-                            () -> {
-                                // Add current broadcast to the latest connected device
-                                mAssistant.addSource(
-                                        cachedDevice.getDevice(),
-                                        mBroadcast.getLatestBluetoothLeBroadcastMetadata(),
-                                        /* isGroupOp= */ true);
-                            });
-                }
-            } else {
-                ArrayList<AudioSharingDeviceItem> deviceItems = new ArrayList<>();
-                for (List<CachedBluetoothDevice> devices : groupedDevices.values()) {
-                    // Use random device in the group within the sharing session to
-                    // represent the group.
-                    CachedBluetoothDevice device = devices.get(0);
-                    if (device.getGroupId() == cachedDevice.getGroupId()) {
-                        continue;
-                    }
-                    deviceItems.add(AudioSharingUtils.buildAudioSharingDeviceItem(device));
-                }
-                // Show audio sharing join dialog when the second eligible (LE audio) remote device
-                // connect and no sharing session.
-                if (deviceItems.size() == 1) {
-                    closeOpeningDialogs();
-                    AudioSharingJoinDialogFragment.show(
-                            mFragment,
-                            deviceItems,
-                            cachedDevice.getName(),
-                            () -> {
-                                mTargetSinks = new ArrayList<>();
-                                for (List<CachedBluetoothDevice> devices :
-                                        groupedDevices.values()) {
-                                    for (CachedBluetoothDevice device : devices) {
-                                        mTargetSinks.add(device.getDevice());
-                                    }
-                                }
-                                mBroadcast.startBroadcast("test", null);
-                            });
-                }
+            handleOnProfileStateChangedForLeAudioDevice(cachedDevice);
+        }
+    }
+
+    private void handleOnProfileStateChangedForNonLeAudioDevice(
+            @NonNull CachedBluetoothDevice cachedDevice) {
+        if (isBroadcasting()) {
+            // Show stop audio sharing dialog when an ineligible (non LE audio) remote device
+            // connected during a sharing session.
+            ThreadUtils.postOnMainThread(
+                    () -> {
+                        closeOpeningDialogs();
+                        AudioSharingStopDialogFragment.show(
+                                mFragment,
+                                cachedDevice.getName(),
+                                () -> mBroadcast.stopBroadcast(mBroadcast.getLatestBroadcastId()));
+                    });
+        } else {
+            // Do nothing for ineligible (non LE audio) remote device when no sharing session.
+            if (DEBUG) {
+                Log.d(
+                        TAG,
+                        "Ignore onProfileConnectionStateChanged for non LE audio without"
+                                + " sharing session");
             }
         }
     }
 
-    /**
-     * Initialize the controller.
-     *
-     * @param fragment The fragment to provide the context and metrics category for {@link
-     *     AudioSharingBluetoothDeviceUpdater} and provide the host for dialogs.
-     */
-    public void init(DashboardFragment fragment) {
-        mFragment = fragment;
-        mBluetoothDeviceUpdater =
-                new AudioSharingBluetoothDeviceUpdater(
-                        fragment.getContext(),
-                        AudioSharingDevicePreferenceController.this,
-                        fragment.getMetricsCategory());
+    private void handleOnProfileStateChangedForLeAudioDevice(
+            @NonNull CachedBluetoothDevice cachedDevice) {
+        Map<Integer, List<CachedBluetoothDevice>> groupedDevices =
+                AudioSharingUtils.fetchConnectedDevicesByGroupId(mLocalBtManager);
+        if (isBroadcasting()) {
+            if (groupedDevices.containsKey(cachedDevice.getGroupId())
+                    && groupedDevices.get(cachedDevice.getGroupId()).stream()
+                            .anyMatch(
+                                    device ->
+                                            AudioSharingUtils.hasBroadcastSource(
+                                                    device, mLocalBtManager))) {
+                Log.d(
+                        TAG,
+                        "Automatically add another device within the same group to the sharing: "
+                                + cachedDevice.getDevice().getAnonymizedAddress());
+                addSourceToTargetDevices(ImmutableList.of(cachedDevice.getDevice()));
+                return;
+            }
+            // Show audio sharing switch or join dialog according to device count in the sharing
+            // session.
+            ArrayList<AudioSharingDeviceItem> deviceItemsInSharingSession =
+                    AudioSharingUtils.buildOrderedConnectedLeadAudioSharingDeviceItem(
+                            mLocalBtManager, groupedDevices, /* filterByInSharing= */ true);
+            // Show audio sharing switch dialog when the third eligible (LE audio) remote device
+            // connected during a sharing session.
+            if (deviceItemsInSharingSession.size() >= 2) {
+                ThreadUtils.postOnMainThread(
+                        () -> {
+                            closeOpeningDialogs();
+                            AudioSharingDisconnectDialogFragment.show(
+                                    mFragment,
+                                    deviceItemsInSharingSession,
+                                    cachedDevice.getName(),
+                                    (AudioSharingDeviceItem item) -> {
+                                        // Remove all sources from the device user clicked
+                                        if (groupedDevices.containsKey(item.getGroupId())) {
+                                            for (CachedBluetoothDevice device :
+                                                    groupedDevices.get(item.getGroupId())) {
+                                                for (BluetoothLeBroadcastReceiveState source :
+                                                        mAssistant.getAllSources(
+                                                                device.getDevice())) {
+                                                    mAssistant.removeSource(
+                                                            device.getDevice(),
+                                                            source.getSourceId());
+                                                }
+                                            }
+                                        }
+                                        // Add current broadcast to the latest connected device
+                                        mAssistant.addSource(
+                                                cachedDevice.getDevice(),
+                                                mBroadcast.getLatestBluetoothLeBroadcastMetadata(),
+                                                /* isGroupOp= */ true);
+                                    });
+                        });
+            } else {
+                // Show audio sharing join dialog when the first or second eligible (LE audio)
+                // remote device connected during a sharing session.
+                ThreadUtils.postOnMainThread(
+                        () -> {
+                            closeOpeningDialogs();
+                            AudioSharingJoinDialogFragment.show(
+                                    mFragment,
+                                    deviceItemsInSharingSession,
+                                    cachedDevice.getName(),
+                                    () -> {
+                                        // Add current broadcast to the latest connected device
+                                        mAssistant.addSource(
+                                                cachedDevice.getDevice(),
+                                                mBroadcast.getLatestBluetoothLeBroadcastMetadata(),
+                                                /* isGroupOp= */ true);
+                                    });
+                        });
+            }
+        } else {
+            ArrayList<AudioSharingDeviceItem> deviceItems = new ArrayList<>();
+            for (List<CachedBluetoothDevice> devices : groupedDevices.values()) {
+                // Use random device in the group within the sharing session to represent the group.
+                CachedBluetoothDevice device = devices.get(0);
+                if (device.getGroupId() == cachedDevice.getGroupId()) {
+                    continue;
+                }
+                deviceItems.add(AudioSharingUtils.buildAudioSharingDeviceItem(device));
+            }
+            // Show audio sharing join dialog when the second eligible (LE audio) remote
+            // device connect and no sharing session.
+            if (deviceItems.size() == 1) {
+                ThreadUtils.postOnMainThread(
+                        () -> {
+                            closeOpeningDialogs();
+                            AudioSharingJoinDialogFragment.show(
+                                    mFragment,
+                                    deviceItems,
+                                    cachedDevice.getName(),
+                                    () -> {
+                                        mTargetSinks = new ArrayList<>();
+                                        for (List<CachedBluetoothDevice> devices :
+                                                groupedDevices.values()) {
+                                            for (CachedBluetoothDevice device : devices) {
+                                                mTargetSinks.add(device.getDevice());
+                                            }
+                                        }
+                                        mBroadcast.startBroadcast("test", null);
+                                    });
+                        });
+            }
+        }
     }
 
     private boolean isLeAudioSupported(CachedBluetoothDevice cachedDevice) {
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingUtils.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingUtils.java
index b5361f2..3d4ef82 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingUtils.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingUtils.java
@@ -41,6 +41,8 @@
 import java.util.Optional;
 import java.util.stream.Collectors;
 
+import javax.annotation.Nullable;
+
 public class AudioSharingUtils {
     private static final String TAG = "AudioSharingUtils";
     private static final boolean DEBUG = BluetoothUtils.D;
@@ -237,7 +239,7 @@
      * @return An Optional containing the active LE Audio device, or an empty Optional if not found.
      */
     public static Optional<CachedBluetoothDevice> getActiveSinkOnAssistant(
-            LocalBluetoothManager manager) {
+            @Nullable LocalBluetoothManager manager) {
         if (manager == null) {
             Log.w(TAG, "getActiveSinksOnAssistant(): LocalBluetoothManager is null!");
             return Optional.empty();
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamButtonController.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamButtonController.java
new file mode 100644
index 0000000..bb729d6
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamButtonController.java
@@ -0,0 +1,66 @@
+/*
+ * 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.audiostreams;
+
+import android.content.Context;
+
+import androidx.annotation.Nullable;
+import androidx.lifecycle.DefaultLifecycleObserver;
+import androidx.preference.PreferenceScreen;
+
+import com.android.settings.R;
+import com.android.settings.core.BasePreferenceController;
+import com.android.settingslib.widget.ActionButtonsPreference;
+
+public class AudioStreamButtonController extends BasePreferenceController
+        implements DefaultLifecycleObserver {
+    private static final String KEY = "audio_stream_button";
+    private @Nullable ActionButtonsPreference mPreference;
+    private int mBroadcastId = -1;
+
+    public AudioStreamButtonController(Context context, String preferenceKey) {
+        super(context, preferenceKey);
+    }
+
+    @Override
+    public final void displayPreference(PreferenceScreen screen) {
+        mPreference = screen.findPreference(getPreferenceKey());
+        if (mPreference != null) {
+            mPreference.setButton1Enabled(true);
+            // TODO(chelseahao): update this based on stream connection state
+            mPreference
+                    .setButton1Text(R.string.bluetooth_device_context_disconnect)
+                    .setButton1Icon(R.drawable.ic_settings_close);
+        }
+        super.displayPreference(screen);
+    }
+
+    @Override
+    public int getAvailabilityStatus() {
+        return AVAILABLE;
+    }
+
+    @Override
+    public String getPreferenceKey() {
+        return KEY;
+    }
+
+    /** Initialize with broadcast id */
+    void init(int broadcastId) {
+        mBroadcastId = broadcastId;
+    }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamDetailsFragment.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamDetailsFragment.java
index 1e69829..e1dc228 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamDetailsFragment.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamDetailsFragment.java
@@ -17,16 +17,28 @@
 package com.android.settings.connecteddevice.audiosharing.audiostreams;
 
 import android.content.Context;
+import android.os.Bundle;
 
 import com.android.settings.R;
 import com.android.settings.dashboard.DashboardFragment;
 
 public class AudioStreamDetailsFragment extends DashboardFragment {
+    static final String BROADCAST_NAME_ARG = "broadcast_name";
+    static final String BROADCAST_ID_ARG = "broadcast_id";
     private static final String TAG = "AudioStreamDetailsFragment";
 
     @Override
     public void onAttach(Context context) {
         super.onAttach(context);
+        Bundle arguments = getArguments();
+        if (arguments != null) {
+            use(AudioStreamHeaderController.class)
+                    .init(
+                            this,
+                            arguments.getString(BROADCAST_NAME_ARG),
+                            arguments.getInt(BROADCAST_ID_ARG));
+            use(AudioStreamButtonController.class).init(arguments.getInt(BROADCAST_ID_ARG));
+        }
     }
 
     @Override
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamHeaderController.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamHeaderController.java
new file mode 100644
index 0000000..89f24bc
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamHeaderController.java
@@ -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.settings.connecteddevice.audiosharing.audiostreams;
+
+import android.content.Context;
+
+import androidx.lifecycle.DefaultLifecycleObserver;
+import androidx.preference.PreferenceScreen;
+
+import com.android.settings.R;
+import com.android.settings.core.BasePreferenceController;
+import com.android.settings.dashboard.DashboardFragment;
+import com.android.settings.widget.EntityHeaderController;
+import com.android.settingslib.widget.LayoutPreference;
+
+import javax.annotation.Nullable;
+
+public class AudioStreamHeaderController extends BasePreferenceController
+        implements DefaultLifecycleObserver {
+    private static final String KEY = "audio_stream_header";
+    private @Nullable EntityHeaderController mHeaderController;
+    private @Nullable DashboardFragment mFragment;
+    private String mBroadcastName = "";
+    private int mBroadcastId = -1;
+
+    public AudioStreamHeaderController(Context context, String preferenceKey) {
+        super(context, preferenceKey);
+    }
+
+    @Override
+    public final void displayPreference(PreferenceScreen screen) {
+        LayoutPreference headerPreference = screen.findPreference(KEY);
+        if (headerPreference != null && mFragment != null) {
+            mHeaderController =
+                    EntityHeaderController.newInstance(
+                            mFragment.getActivity(),
+                            mFragment,
+                            headerPreference.findViewById(R.id.entity_header));
+            if (mBroadcastName != null) {
+                mHeaderController.setLabel(mBroadcastName);
+            }
+            mHeaderController.setIcon(
+                    screen.getContext().getDrawable(R.drawable.ic_bt_audio_sharing));
+            // TODO(chelseahao): update this based on stream connection state
+            mHeaderController.setSummary("Listening now");
+            mHeaderController.done(true);
+            screen.addPreference(headerPreference);
+        }
+        super.displayPreference(screen);
+    }
+
+    @Override
+    public int getAvailabilityStatus() {
+        return AVAILABLE;
+    }
+
+    @Override
+    public String getPreferenceKey() {
+        return KEY;
+    }
+
+    /** Initialize with {@link AudioStreamDetailsFragment} and broadcast name and id */
+    void init(
+            AudioStreamDetailsFragment audioStreamDetailsFragment,
+            String broadcastName,
+            int broadcastId) {
+        mFragment = audioStreamDetailsFragment;
+        mBroadcastName = broadcastName;
+        mBroadcastId = broadcastId;
+    }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsHelper.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsHelper.java
index 5acbc1f..198e8e5 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsHelper.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsHelper.java
@@ -80,8 +80,8 @@
                         });
     }
 
-    /** Removes all sources from LE broadcasts associated for all active sinks. */
-    void removeSource() {
+    /** Removes sources from LE broadcasts associated for all active sinks based on broadcast Id. */
+    void removeSource(int broadcastId) {
         if (mLeBroadcastAssistant == null) {
             Log.w(TAG, "removeSource(): LeBroadcastAssistant is null!");
             return;
@@ -93,14 +93,17 @@
                                 if (DEBUG) {
                                     Log.d(
                                             TAG,
-                                            "removeSource(): remove all sources from sink : "
+                                            "removeSource(): remove all sources with broadcast id :"
+                                                    + broadcastId
+                                                    + " from sink : "
                                                     + sink.getAddress());
                                 }
-                                var sources = mLeBroadcastAssistant.getAllSources(sink);
-                                if (!sources.isEmpty()) {
-                                    mLeBroadcastAssistant.removeSource(
-                                            sink, sources.get(0).getSourceId());
-                                }
+                                mLeBroadcastAssistant.getAllSources(sink).stream()
+                                        .filter(state -> state.getBroadcastId() == broadcastId)
+                                        .forEach(
+                                                state ->
+                                                        mLeBroadcastAssistant.removeSource(
+                                                                sink, state.getSourceId()));
                             }
                         });
     }
@@ -121,6 +124,12 @@
         return mLeBroadcastAssistant;
     }
 
+    static boolean isConnected(BluetoothLeBroadcastReceiveState state) {
+        return state.getPaSyncState() == BluetoothLeBroadcastReceiveState.PA_SYNC_STATE_SYNCHRONIZED
+                && state.getBigEncryptionState()
+                        == BluetoothLeBroadcastReceiveState.BIG_ENCRYPTION_STATE_DECRYPTING;
+    }
+
     private static List<BluetoothDevice> getActiveSinksOnAssistant(
             @Nullable LocalBluetoothManager manager) {
         if (manager == null) {
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java
index 45f0c2f..3c005b2 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java
@@ -22,9 +22,9 @@
 import android.app.settings.SettingsEnums;
 import android.bluetooth.BluetoothLeBroadcastMetadata;
 import android.bluetooth.BluetoothLeBroadcastReceiveState;
+import android.bluetooth.BluetoothProfile;
 import android.content.Context;
 import android.os.Bundle;
-import android.provider.Settings;
 import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
@@ -42,8 +42,11 @@
 import com.android.settings.connecteddevice.audiosharing.AudioSharingUtils;
 import com.android.settings.core.BasePreferenceController;
 import com.android.settings.core.SubSettingLauncher;
+import com.android.settingslib.bluetooth.BluetoothCallback;
 import com.android.settingslib.bluetooth.BluetoothUtils;
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
 import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
 import com.android.settingslib.utils.ThreadUtils;
 
 import java.nio.charset.StandardCharsets;
@@ -57,11 +60,22 @@
         implements DefaultLifecycleObserver {
     private static final String TAG = "AudioStreamsProgressCategoryController";
     private static final boolean DEBUG = BluetoothUtils.D;
+    private final BluetoothCallback mBluetoothCallback =
+            new BluetoothCallback() {
+                @Override
+                public void onActiveDeviceChanged(
+                        @Nullable CachedBluetoothDevice activeDevice, int bluetoothProfile) {
+                    if (bluetoothProfile == BluetoothProfile.LE_AUDIO) {
+                        mExecutor.execute(() -> init(activeDevice != null));
+                    }
+                }
+            };
 
     private final Executor mExecutor;
     private final AudioStreamsBroadcastAssistantCallback mBroadcastAssistantCallback;
     private final AudioStreamsHelper mAudioStreamsHelper;
     private final @Nullable LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant;
+    private final @Nullable LocalBluetoothManager mBluetoothManager;
     private final ConcurrentHashMap<Integer, AudioStreamPreference> mBroadcastIdToPreferenceMap =
             new ConcurrentHashMap<>();
     private AudioStreamsProgressCategoryPreference mCategoryPreference;
@@ -69,7 +83,8 @@
     public AudioStreamsProgressCategoryController(Context context, String preferenceKey) {
         super(context, preferenceKey);
         mExecutor = Executors.newSingleThreadExecutor();
-        mAudioStreamsHelper = new AudioStreamsHelper(Utils.getLocalBtManager(mContext));
+        mBluetoothManager = Utils.getLocalBtManager(mContext);
+        mAudioStreamsHelper = new AudioStreamsHelper(mBluetoothManager);
         mLeBroadcastAssistant = mAudioStreamsHelper.getLeBroadcastAssistant();
         mBroadcastAssistantCallback = new AudioStreamsBroadcastAssistantCallback(this);
     }
@@ -87,48 +102,24 @@
 
     @Override
     public void onStart(@NonNull LifecycleOwner owner) {
-        if (mLeBroadcastAssistant == null) {
-            Log.w(TAG, "onStart(): LeBroadcastAssistant is null!");
-            return;
-        }
-        mBroadcastIdToPreferenceMap.clear();
-        if (mCategoryPreference != null) {
-            mCategoryPreference.removeAll();
+        if (mBluetoothManager != null) {
+            mBluetoothManager.getEventManager().registerCallback(mBluetoothCallback);
         }
         mExecutor.execute(
                 () -> {
-                    mLeBroadcastAssistant.registerServiceCallBack(
-                            mExecutor, mBroadcastAssistantCallback);
-                    if (DEBUG) {
-                        Log.d(TAG, "scanAudioStreamsStart()");
-                    }
-                    mLeBroadcastAssistant.startSearchingForSources(emptyList());
-                    // Display currently connected streams
-                    var unused =
-                            ThreadUtils.postOnBackgroundThread(
-                                    () ->
-                                            mAudioStreamsHelper
-                                                    .getAllSources()
-                                                    .forEach(this::handleSourceConnected));
+                    boolean hasActive =
+                            AudioSharingUtils.getActiveSinkOnAssistant(mBluetoothManager)
+                                    .isPresent();
+                    init(hasActive);
                 });
     }
 
     @Override
     public void onStop(@NonNull LifecycleOwner owner) {
-        if (mLeBroadcastAssistant == null) {
-            Log.w(TAG, "onStop(): LeBroadcastAssistant is null!");
-            return;
+        if (mBluetoothManager != null) {
+            mBluetoothManager.getEventManager().unregisterCallback(mBluetoothCallback);
         }
-        mExecutor.execute(
-                () -> {
-                    if (mLeBroadcastAssistant.isSearchInProgress()) {
-                        if (DEBUG) {
-                            Log.d(TAG, "scanAudioStreamsStop()");
-                        }
-                        mLeBroadcastAssistant.stopSearchingForSources();
-                    }
-                    mLeBroadcastAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback);
-                });
+        mExecutor.execute(this::stopScanning);
     }
 
     void setScanning(boolean isScanning) {
@@ -142,7 +133,10 @@
         Preference.OnPreferenceClickListener addSourceOrShowDialog =
                 preference -> {
                     if (DEBUG) {
-                        Log.d(TAG, "preferenceClicked(): attempt to join broadcast");
+                        Log.d(
+                                TAG,
+                                "preferenceClicked(): attempt to join broadcast id : "
+                                        + source.getBroadcastId());
                     }
                     if (source.isEncrypted()) {
                         ThreadUtils.postOnMainThread(
@@ -177,11 +171,13 @@
                         }
                     });
         }
-        mAudioStreamsHelper.removeSource();
+        mAudioStreamsHelper.removeSource(broadcastId);
     }
 
     void handleSourceConnected(BluetoothLeBroadcastReceiveState state) {
-        // TODO(chelseahao): only continue when the state indicates a successful connection
+        if (!AudioStreamsHelper.isConnected(state)) {
+            return;
+        }
         mBroadcastIdToPreferenceMap.compute(
                 state.getBroadcastId(),
                 (k, v) -> {
@@ -194,7 +190,7 @@
                     ThreadUtils.postOnMainThread(
                             () -> {
                                 preference.setIsConnected(
-                                        true, p -> launchDetailFragment((AudioStreamPreference) p));
+                                        true, p -> launchDetailFragment(state.getBroadcastId()));
                                 if (mCategoryPreference != null && !existed) {
                                     mCategoryPreference.addPreference(preference);
                                 }
@@ -208,11 +204,73 @@
         AudioSharingUtils.toastMessage(mContext, msg);
     }
 
-    private boolean launchDetailFragment(AudioStreamPreference preference) {
+    private void init(boolean hasActive) {
+        mBroadcastIdToPreferenceMap.clear();
+        ThreadUtils.postOnMainThread(
+                () -> {
+                    if (mCategoryPreference != null) {
+                        mCategoryPreference.removeAll();
+                        mCategoryPreference.setVisible(hasActive);
+                    }
+                });
+        if (hasActive) {
+            startScanning();
+        } else {
+            stopScanning();
+        }
+    }
+
+    private void startScanning() {
+        if (mLeBroadcastAssistant == null) {
+            Log.w(TAG, "startScanning(): LeBroadcastAssistant is null!");
+            return;
+        }
+        if (mLeBroadcastAssistant.isSearchInProgress()) {
+            showToast("Failed to start scanning, please try again.");
+            return;
+        }
+        if (DEBUG) {
+            Log.d(TAG, "startScanning()");
+        }
+        mLeBroadcastAssistant.registerServiceCallBack(mExecutor, mBroadcastAssistantCallback);
+        mLeBroadcastAssistant.startSearchingForSources(emptyList());
+
+        // Display currently connected streams
+        var unused =
+                ThreadUtils.postOnBackgroundThread(
+                        () ->
+                                mAudioStreamsHelper
+                                        .getAllSources()
+                                        .forEach(this::handleSourceConnected));
+    }
+
+    private void stopScanning() {
+        if (mLeBroadcastAssistant == null) {
+            Log.w(TAG, "stopScanning(): LeBroadcastAssistant is null!");
+            return;
+        }
+        if (mLeBroadcastAssistant.isSearchInProgress()) {
+            if (DEBUG) {
+                Log.d(TAG, "stopScanning()");
+            }
+            mLeBroadcastAssistant.stopSearchingForSources();
+        }
+        mLeBroadcastAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback);
+    }
+
+    private boolean launchDetailFragment(int broadcastId) {
+        if (!mBroadcastIdToPreferenceMap.containsKey(broadcastId)) {
+            Log.w(
+                    TAG,
+                    "launchDetailFragment(): broadcastId not exist in BroadcastIdToPreferenceMap!");
+            return false;
+        }
+        AudioStreamPreference preference = mBroadcastIdToPreferenceMap.get(broadcastId);
+
         Bundle broadcast = new Bundle();
         broadcast.putString(
-                Settings.Secure.BLUETOOTH_LE_BROADCAST_PROGRAM_INFO,
-                (String) preference.getTitle());
+                AudioStreamDetailsFragment.BROADCAST_NAME_ARG, (String) preference.getTitle());
+        broadcast.putInt(AudioStreamDetailsFragment.BROADCAST_ID_ARG, broadcastId);
 
         new SubSettingLauncher(mContext)
                 .setTitleText("Audio stream details")
@@ -240,8 +298,8 @@
                                 (dialog, which) -> {
                                     var code =
                                             ((EditText)
-                                                    layout.requireViewById(
-                                                            R.id.broadcast_edit_text))
+                                                            layout.requireViewById(
+                                                                    R.id.broadcast_edit_text))
                                                     .getText()
                                                     .toString();
                                     mAudioStreamsHelper.addSource(
diff --git a/src/com/android/settings/dashboard/profileselector/ProfileSelectFragment.java b/src/com/android/settings/dashboard/profileselector/ProfileSelectFragment.java
index 0348e11..8279588 100644
--- a/src/com/android/settings/dashboard/profileselector/ProfileSelectFragment.java
+++ b/src/com/android/settings/dashboard/profileselector/ProfileSelectFragment.java
@@ -41,6 +41,8 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentManager;
+import androidx.lifecycle.Lifecycle;
 import androidx.recyclerview.widget.RecyclerView;
 import androidx.viewpager2.adapter.FragmentStateAdapter;
 import androidx.viewpager2.widget.ViewPager2;
@@ -357,6 +359,11 @@
         return fragment;
     }
 
+    @VisibleForTesting
+    void setViewPager(ViewPager2 viewPager) {
+        mViewPager = viewPager;
+    }
+
     interface FragmentConstructor {
         Fragment constructAndGetFragment();
     }
@@ -376,6 +383,15 @@
             mChildFragments = fragment.getFragments();
         }
 
+        @VisibleForTesting
+        ViewPagerAdapter(
+                @NonNull FragmentManager fragmentManager,
+                @NonNull Lifecycle lifecycle,
+                ProfileSelectFragment profileSelectFragment) {
+            super(fragmentManager, lifecycle);
+            mChildFragments = profileSelectFragment.getFragments();
+        }
+
         @Override
         public Fragment createFragment(int position) {
             return mChildFragments[position];
@@ -386,7 +402,8 @@
             return mChildFragments.length;
         }
 
-        private int getTabForPosition(int position) {
+        @VisibleForTesting
+        int getTabForPosition(int position) {
             if (position >= mChildFragments.length) {
                 Log.e(TAG, "tab requested for out of bound position " + position);
                 return PERSONAL_TAB;
diff --git a/src/com/android/settings/development/BackAnimationPreferenceController.java b/src/com/android/settings/development/BackAnimationPreferenceController.java
index 95ffc25..8c731a4 100644
--- a/src/com/android/settings/development/BackAnimationPreferenceController.java
+++ b/src/com/android/settings/development/BackAnimationPreferenceController.java
@@ -18,6 +18,7 @@
 
 import static com.android.window.flags.Flags.predictiveBackSystemAnimations;
 
+import android.annotation.Nullable;
 import android.content.Context;
 import android.provider.Settings;
 
@@ -28,8 +29,6 @@
 import com.android.settings.core.PreferenceControllerMixin;
 import com.android.settingslib.development.DeveloperOptionsPreferenceController;
 
-import java.util.Objects;
-
 /**
  * PreferenceController for enabling/disabling animation related to back button and back gestures.
  */
@@ -51,9 +50,8 @@
 
 
     public BackAnimationPreferenceController(Context context,
-            DevelopmentSettingsDashboardFragment fragment) {
+            @Nullable DevelopmentSettingsDashboardFragment fragment) {
         super(context);
-        Objects.requireNonNull(fragment);
         mFragment = fragment;
     }
 
diff --git a/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java b/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java
index a483f9f..b52409d 100644
--- a/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java
+++ b/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java
@@ -737,7 +737,7 @@
         controllers.add(new OverlaySettingsPreferenceController(context));
         controllers.add(new StylusHandwritingPreferenceController(context));
         controllers.add(new IngressRateLimitPreferenceController((context)));
-        // controllers.add(new BackAnimationPreferenceController(context, fragment));
+        controllers.add(new BackAnimationPreferenceController(context, fragment));
         controllers.add(new PhantomProcessPreferenceController(context));
         controllers.add(new ContrastPreferenceController(
                 context, context.getSystemService(UiModeManager.class)));
diff --git a/src/com/android/settings/inputmethod/NewKeyboardLayoutPickerContent.java b/src/com/android/settings/inputmethod/NewKeyboardLayoutPickerContent.java
index 11740ec..e934964 100644
--- a/src/com/android/settings/inputmethod/NewKeyboardLayoutPickerContent.java
+++ b/src/com/android/settings/inputmethod/NewKeyboardLayoutPickerContent.java
@@ -27,6 +27,20 @@
 public class NewKeyboardLayoutPickerContent extends DashboardFragment {
 
     private static final String TAG = "KeyboardLayoutPicker";
+    private NewKeyboardLayoutPickerController mNewKeyboardLayoutPickerController;
+    private ControllerUpdateCallback mControllerUpdateCallback;
+
+    public interface ControllerUpdateCallback {
+        /**
+         * Called when mNewKeyBoardLayoutPickerController been initialized.
+         */
+        void onControllerUpdated(NewKeyboardLayoutPickerController
+                newKeyboardLayoutPickerController);
+    }
+
+    public void setControllerUpdateCallback(ControllerUpdateCallback controllerUpdateCallback) {
+        this.mControllerUpdateCallback = controllerUpdateCallback;
+    }
 
     @Override
     public void onAttach(Context context) {
@@ -40,7 +54,11 @@
             getActivity().finish();
             return;
         }
-        use(NewKeyboardLayoutPickerController.class).initialize(this);
+        mNewKeyboardLayoutPickerController = use(NewKeyboardLayoutPickerController.class);
+        mNewKeyboardLayoutPickerController.initialize(this);
+        if (mControllerUpdateCallback != null) {
+            mControllerUpdateCallback.onControllerUpdated(mNewKeyboardLayoutPickerController);
+        }
     }
 
     @Override
@@ -56,4 +74,8 @@
     protected int getPreferenceScreenResId() {
         return R.xml.new_keyboard_layout_picker_fragment;
     }
+
+    public NewKeyboardLayoutPickerController getController() {
+        return mNewKeyboardLayoutPickerController;
+    }
 }
diff --git a/src/com/android/settings/inputmethod/NewKeyboardLayoutPickerController.java b/src/com/android/settings/inputmethod/NewKeyboardLayoutPickerController.java
index 9545276..ac8037f 100644
--- a/src/com/android/settings/inputmethod/NewKeyboardLayoutPickerController.java
+++ b/src/com/android/settings/inputmethod/NewKeyboardLayoutPickerController.java
@@ -59,6 +59,7 @@
     private String mFinalSelectedLayout;
     private String mLayout;
     private MetricsFeatureProvider mMetricsFeatureProvider;
+    private KeyboardLayoutSelectedCallback mKeyboardLayoutSelectedCallback;
 
     public NewKeyboardLayoutPickerController(Context context, String key) {
         super(context, key);
@@ -100,7 +101,7 @@
 
     @Override
     public void onStop() {
-        if (!mLayout.equals(mFinalSelectedLayout)) {
+        if (mLayout != null && !mLayout.equals(mFinalSelectedLayout)) {
             String change = "From:" + mLayout + ", to:" + mFinalSelectedLayout;
             mMetricsFeatureProvider.action(
                     mContext, SettingsEnums.ACTION_PK_LAYOUT_CHANGED, change);
@@ -121,6 +122,14 @@
         return AVAILABLE;
     }
 
+    /**
+     * Registers {@link KeyboardLayoutSelectedCallback} and get updated.
+     */
+    public void registerKeyboardSelectedCallback(KeyboardLayoutSelectedCallback
+            keyboardLayoutSelectedCallback) {
+        this.mKeyboardLayoutSelectedCallback = keyboardLayoutSelectedCallback;
+    }
+
     @Override
     public boolean handlePreferenceTreeClick(Preference preference) {
         if (!(preference instanceof TickButtonPreference)) {
@@ -128,6 +137,9 @@
         }
 
         final TickButtonPreference pref = (TickButtonPreference) preference;
+        if (mKeyboardLayoutSelectedCallback != null && mPreferenceMap.containsKey(preference)) {
+            mKeyboardLayoutSelectedCallback.onSelected(mPreferenceMap.get(preference));
+        }
         pref.setSelected(true);
         if (mPreviousSelection != null && !mPreviousSelection.equals(preference.getKey())) {
             TickButtonPreference preSelectedPref = mScreen.findPreference(mPreviousSelection);
@@ -166,6 +178,9 @@
             pref.setTitle(layout.getLabel());
 
             if (mLayout.equals(layout.getLabel())) {
+                if (mKeyboardLayoutSelectedCallback != null) {
+                    mKeyboardLayoutSelectedCallback.onSelected(layout);
+                }
                 pref.setSelected(true);
                 mPreviousSelection = layout.getDescriptor();
             }
@@ -200,4 +215,11 @@
         }
         return label;
     }
+
+    public interface KeyboardLayoutSelectedCallback {
+        /**
+         * Called when KeyboardLayout been selected.
+         */
+        void onSelected(KeyboardLayout keyboardLayout);
+    }
 }
diff --git a/src/com/android/settings/inputmethod/NewKeyboardLayoutPickerFragment.java b/src/com/android/settings/inputmethod/NewKeyboardLayoutPickerFragment.java
index 88cacd2..f583971 100644
--- a/src/com/android/settings/inputmethod/NewKeyboardLayoutPickerFragment.java
+++ b/src/com/android/settings/inputmethod/NewKeyboardLayoutPickerFragment.java
@@ -16,35 +16,75 @@
 
 package com.android.settings.inputmethod;
 
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
+
+import android.graphics.drawable.Drawable;
+import android.hardware.input.InputManager;
+import android.hardware.input.KeyboardLayout;
 import android.os.Bundle;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
+import android.widget.ImageView;
 
 import androidx.fragment.app.Fragment;
 
 import com.android.settings.R;
 
+//TODO: b/316243168 - [Physical Keyboard Setting] Refactor NewKeyboardLayoutPickerFragment
 public class NewKeyboardLayoutPickerFragment extends Fragment {
+    private static final int DEFAULT_KEYBOARD_PREVIEW_WIDTH = 1630;
+    private static final int DEFAULT_KEYBOARD_PREVIEW_HEIGHT = 540;
+
+    private ImageView mKeyboardLayoutPreview;
+    private InputManager mInputManager;
+    private final NewKeyboardLayoutPickerController.KeyboardLayoutSelectedCallback
+            mKeyboardLayoutSelectedCallback =
+            new NewKeyboardLayoutPickerController.KeyboardLayoutSelectedCallback() {
+                @Override
+                public void onSelected(KeyboardLayout keyboardLayout) {
+                    if (mInputManager != null && mKeyboardLayoutPreview != null) {
+                        Drawable previewDrawable = mInputManager.getKeyboardLayoutPreview(
+                                keyboardLayout,
+                                DEFAULT_KEYBOARD_PREVIEW_WIDTH, DEFAULT_KEYBOARD_PREVIEW_HEIGHT);
+                        mKeyboardLayoutPreview.setVisibility(
+                                previewDrawable == null ? GONE : VISIBLE);
+                        if (previewDrawable != null) {
+                            mKeyboardLayoutPreview.setImageDrawable(previewDrawable);
+                        }
+                    }
+                }
+            };
+
+    private final NewKeyboardLayoutPickerContent.ControllerUpdateCallback
+            mControllerUpdateCallback =
+                    newKeyboardLayoutPickerController -> {
+                        if (newKeyboardLayoutPickerController != null) {
+                            newKeyboardLayoutPickerController.registerKeyboardSelectedCallback(
+                                    mKeyboardLayoutSelectedCallback);
+                        }
+                    };
 
     @Override
     public View onCreateView(LayoutInflater inflater, ViewGroup container,
             Bundle savedInstanceState) {
-
+        mInputManager = requireContext().getSystemService(InputManager.class);
         ViewGroup fragmentView = (ViewGroup) inflater.inflate(
                 R.layout.keyboard_layout_picker, container, false);
+        mKeyboardLayoutPreview = fragmentView.findViewById(R.id.keyboard_layout_preview);
         getActivity().getSupportFragmentManager()
                 .beginTransaction()
                 .replace(R.id.keyboard_layout_title, new NewKeyboardLayoutPickerTitle())
                 .commit();
 
         NewKeyboardLayoutPickerContent fragment = new NewKeyboardLayoutPickerContent();
+        fragment.setControllerUpdateCallback(mControllerUpdateCallback);
         fragment.setArguments(getArguments());
         getActivity().getSupportFragmentManager()
                 .beginTransaction()
                 .replace(R.id.keyboard_layouts, fragment)
                 .commit();
-
         return fragmentView;
     }
 }
diff --git a/src/com/android/settings/network/apn/ApnEditPageProvider.kt b/src/com/android/settings/network/apn/ApnEditPageProvider.kt
index bb19c59..52066a1 100644
--- a/src/com/android/settings/network/apn/ApnEditPageProvider.kt
+++ b/src/com/android/settings/network/apn/ApnEditPageProvider.kt
@@ -39,7 +39,6 @@
 import com.android.settings.network.apn.ApnNetworkTypes.getNetworkTypeSelectedOptionsState
 import com.android.settingslib.spa.framework.common.SettingsPageProvider
 import com.android.settingslib.spa.framework.compose.LocalNavController
-import com.android.settingslib.spa.framework.compose.OnBackEffect
 import com.android.settingslib.spa.widget.editor.SettingsExposedDropdownMenuBox
 import com.android.settingslib.spa.widget.editor.SettingsExposedDropdownMenuCheckBox
 import com.android.settingslib.spa.widget.editor.SettingsOutlinedTextField
@@ -98,29 +97,35 @@
     val networkTypeSelectedOptionsState = remember {
         getNetworkTypeSelectedOptionsState(apnData.networkType)
     }
-    OnBackEffect{
-        validateAndSaveApnData(
-            apnDataInit,
-            apnData,
-            context,
-            uriInit,
-            networkTypeSelectedOptionsState
-        )
-    }
     val navController = LocalNavController.current
     RegularScaffold(
-        title = if(apnDataInit.newApn) stringResource(id = R.string.apn_add) else stringResource(id = R.string.apn_edit),
+        title = if (apnDataInit.newApn) stringResource(id = R.string.apn_add) else stringResource(id = R.string.apn_edit),
+        actions = {
+            IconButton(onClick = {
+                if (!apnData.validEnabled) apnData = apnData.copy(validEnabled = true)
+                val valid = validateAndSaveApnData(
+                    apnDataInit,
+                    apnData,
+                    context,
+                    uriInit,
+                    networkTypeSelectedOptionsState
+                )
+                if (valid) navController.navigateBack()
+            }) { Icon(imageVector = Icons.Outlined.Done, contentDescription = null) }
+        },
     ) {
         Column {
             SettingsOutlinedTextField(
                 value = apnData.name,
                 label = stringResource(R.string.apn_name),
-                enabled = apnData.nameEnabled
+                enabled = apnData.nameEnabled,
+                errorMessage = validateName(apnData.validEnabled, apnData.name, context)
             ) { apnData = apnData.copy(name = it) }
             SettingsOutlinedTextField(
                 value = apnData.apn,
                 label = stringResource(R.string.apn_apn),
-                enabled = apnData.apnEnabled
+                enabled = apnData.apnEnabled,
+                errorMessage = validateAPN(apnData.validEnabled, apnData.apn, context)
             ) { apnData = apnData.copy(apn = it) }
             SettingsOutlinedTextField(
                 value = apnData.proxy,
@@ -150,7 +155,7 @@
             SettingsOutlinedTextField(
                 value = apnData.mmsc,
                 label = stringResource(R.string.apn_mmsc),
-                errorMessage = validateMMSC(apnData.mmsc, context),
+                errorMessage = validateMMSC(apnData.validEnabled, apnData.mmsc, context),
                 enabled = apnData.mmscEnabled
             ) { apnData = apnData.copy(mmsc = it) }
             SettingsOutlinedTextField(
@@ -172,7 +177,11 @@
             SettingsOutlinedTextField(
                 value = apnData.apnType,
                 label = stringResource(R.string.apn_type),
-                enabled = apnData.apnTypeEnabled
+                enabled = apnData.apnTypeEnabled,
+                errorMessage = validateAPNType(
+                    apnData.validEnabled, apnData.apnType,
+                    apnData.customizedConfig.readOnlyApnTypes, context
+                )
             ) { apnData = apnData.copy(apnType = updateApnType(apnData.copy(apnType = it))) }
             SettingsExposedDropdownMenuBox(
                 label = stringResource(R.string.apn_protocol),
@@ -209,7 +218,6 @@
                         override val title = stringResource(R.string.menu_delete)
                         override val onClick = {
                             deleteApn(uriInit, context)
-                            apnData = apnData.copy(saveEnabled = false)
                             navController.navigateBack()
                         }
                     }
diff --git a/src/com/android/settings/network/apn/ApnStatus.kt b/src/com/android/settings/network/apn/ApnStatus.kt
index 668ea9b..38c6684 100644
--- a/src/com/android/settings/network/apn/ApnStatus.kt
+++ b/src/com/android/settings/network/apn/ApnStatus.kt
@@ -69,7 +69,7 @@
     val networkTypeEnabled: Boolean = true,
     val newApn: Boolean = false,
     val subId: Int = -1,
-    val saveEnabled: Boolean = true,
+    val validEnabled: Boolean = false,
     val customizedConfig: CustomizedConfig = CustomizedConfig()
 ) {
     fun getContentValues(context: Context): ContentValues {
@@ -96,7 +96,8 @@
         values.put(Telephony.Carriers.EDITED_STATUS, Telephony.Carriers.USER_EDITED)
         if (newApn) {
             val simCarrierId =
-                context.getSystemService(TelephonyManager::class.java)!!.createForSubscriptionId(subId)
+                context.getSystemService(TelephonyManager::class.java)!!
+                    .createForSubscriptionId(subId)
                     .getSimCarrierId()
             values.put(Telephony.Carriers.CARRIER_ID, simCarrierId)
         }
@@ -231,22 +232,12 @@
     uriInit: Uri,
     networkTypeSelectedOptionsState: SnapshotStateList<Int>
 ): Boolean {
-    // Can not be saved
-    if (!apnData.saveEnabled) {
-        return false
-    }
     // Nothing to do if it's a read only APN
     if (apnData.customizedConfig.readOnlyApn) {
         return true
     }
-    var errorMsg = validateApnData(apnData, context)
+    val errorMsg = validateApnData(apnData, context)
     if (errorMsg != null) {
-        //TODO: showError(this)
-        return false
-    }
-    errorMsg = validateMMSC(apnData.mmsc, context)
-    if (errorMsg != null) {
-        //TODO: showError(this)
         return false
     }
     val newApnData = apnData.copy(networkType = getNetworkType(networkTypeSelectedOptionsState))
@@ -268,37 +259,23 @@
  * @return An error message if the apn data is invalid, otherwise return null.
  */
 fun validateApnData(apnData: ApnData, context: Context): String? {
-    var errorMsg: String? = null
+    var errorMsg: String?
     val name = apnData.name
     val apn = apnData.apn
-    if (name == "") {
-        errorMsg = context.resources.getString(R.string.error_name_empty)
+    errorMsg = if (name == "") {
+        context.resources.getString(R.string.error_name_empty)
     } else if (apn == "") {
-        errorMsg = context.resources.getString(R.string.error_apn_empty)
+        context.resources.getString(R.string.error_apn_empty)
+    } else {
+        validateMMSC(apnData.validEnabled, apnData.mmsc, context)
     }
     if (errorMsg == null) {
-        // if carrier does not allow editing certain apn types, make sure type does not include
-        // those
-        if (!ArrayUtils.isEmpty(apnData.customizedConfig.readOnlyApnTypes)
-            && apnTypesMatch(
-                apnData.customizedConfig.readOnlyApnTypes,
-                getUserEnteredApnType(apnData.apnType, apnData.customizedConfig.readOnlyApnTypes)
-            )
-        ) {
-            val stringBuilder = StringBuilder()
-            for (type in apnData.customizedConfig.readOnlyApnTypes) {
-                stringBuilder.append(type).append(", ")
-                Log.d(TAG, "validateApnData: appending type: $type")
-            }
-            // remove last ", "
-            if (stringBuilder.length >= 2) {
-                stringBuilder.delete(stringBuilder.length - 2, stringBuilder.length)
-            }
-            errorMsg = String.format(
-                context.resources.getString(R.string.error_adding_apn_type),
-                stringBuilder
-            )
-        }
+        errorMsg = validateAPNType(
+            apnData.validEnabled,
+            apnData.apnType,
+            apnData.customizedConfig.readOnlyApnTypes,
+            context
+        )
     }
     return errorMsg
 }
@@ -536,7 +513,39 @@
     contentResolver.delete(uri, null, null)
 }
 
-fun validateMMSC(mmsc: String, context: Context): String? {
-    return if (mmsc.matches(Regex("^https?:\\/\\/.+"))) null
-            else context.resources.getString(R.string.error_mmsc_valid)
+fun validateMMSC(validEnabled: Boolean, mmsc: String, context: Context): String? {
+    return if (validEnabled && !mmsc.matches(Regex("^https?:\\/\\/.+")))
+        context.resources.getString(R.string.error_mmsc_valid)
+    else null
+}
+
+fun validateName(validEnabled: Boolean, name: String, context: Context): String? {
+    return if (validEnabled && (name == "")) context.resources.getString(R.string.error_name_empty)
+    else null
+}
+
+fun validateAPN(validEnabled: Boolean, apn: String, context: Context): String? {
+    return if (validEnabled && (apn == "")) context.resources.getString(R.string.error_apn_empty)
+    else null
+}
+
+fun validateAPNType(
+    validEnabled: Boolean,
+    apnType: String,
+    readOnlyApnTypes: List<String>,
+    context: Context
+): String? {
+    // if carrier does not allow editing certain apn types, make sure type does not include those
+    if (validEnabled && !ArrayUtils.isEmpty(readOnlyApnTypes)
+        && apnTypesMatch(
+            readOnlyApnTypes,
+            getUserEnteredApnType(apnType, readOnlyApnTypes)
+        )
+    ) {
+        return String.format(
+            context.resources.getString(R.string.error_adding_apn_type),
+            readOnlyApnTypes.joinToString(", ")
+        )
+    }
+    return null
 }
\ No newline at end of file
diff --git a/src/com/android/settings/privatespace/PrivateSpaceMaintainer.java b/src/com/android/settings/privatespace/PrivateSpaceMaintainer.java
index b5e76920..b8f140f 100644
--- a/src/com/android/settings/privatespace/PrivateSpaceMaintainer.java
+++ b/src/com/android/settings/privatespace/PrivateSpaceMaintainer.java
@@ -18,6 +18,7 @@
 
 import static android.os.UserManager.USER_TYPE_PROFILE_PRIVATE;
 import static android.provider.Settings.Secure.HIDE_PRIVATESPACE_ENTRY_POINT;
+import static android.provider.Settings.Secure.USER_SETUP_COMPLETE;
 
 import android.app.ActivityManager;
 import android.app.IActivityManager;
@@ -96,6 +97,7 @@
 
             IActivityManager am = ActivityManager.getService();
             try {
+                //TODO(b/313926659): To check and handle failure of startProfile
                 am.startProfile(mUserHandle.getIdentifier());
             } catch (RemoteException e) {
                 Log.e(TAG, "Failed to start private profile");
@@ -104,6 +106,7 @@
 
             Log.i(TAG, "Private space created with id: " + mUserHandle.getIdentifier());
             resetPrivateSpaceSettings();
+            setUserSetupComplete();
         }
         return true;
     }
@@ -250,4 +253,14 @@
     private void resetPrivateSpaceSettings() {
         setHidePrivateSpaceEntryPointSetting(HIDE_PRIVATE_SPACE_ENTRY_POINT_DISABLED_VAL);
     }
+
+    /**
+     * Sets the USER_SETUP_COMPLETE for private profile on which device theme is applied to the
+     * profile.
+     */
+    @GuardedBy("this")
+    private void setUserSetupComplete() {
+        Settings.Secure.putIntForUser(mContext.getContentResolver(), USER_SETUP_COMPLETE,
+                1, mUserHandle.getIdentifier());
+    }
 }
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/AvailableMediaDeviceGroupControllerTest.java b/tests/robotests/src/com/android/settings/connecteddevice/AvailableMediaDeviceGroupControllerTest.java
index 06dd42b..e5964d0 100644
--- a/tests/robotests/src/com/android/settings/connecteddevice/AvailableMediaDeviceGroupControllerTest.java
+++ b/tests/robotests/src/com/android/settings/connecteddevice/AvailableMediaDeviceGroupControllerTest.java
@@ -17,20 +17,29 @@
 
 import static com.android.settings.core.BasePreferenceController.AVAILABLE_UNSEARCHABLE;
 import static com.android.settings.core.BasePreferenceController.UNSUPPORTED_ON_DEVICE;
+
 import static com.google.common.truth.Truth.assertThat;
+
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 import static org.robolectric.shadows.ShadowLooper.shadowMainLooper;
 
+import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothLeBroadcastAssistant;
 import android.bluetooth.BluetoothProfile;
+import android.bluetooth.BluetoothStatusCodes;
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.media.AudioManager;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 
 import androidx.appcompat.app.AlertDialog;
 import androidx.fragment.app.FragmentActivity;
@@ -42,6 +51,7 @@
 import com.android.settings.R;
 import com.android.settings.bluetooth.AvailableMediaBluetoothDeviceUpdater;
 import com.android.settings.bluetooth.Utils;
+import com.android.settings.flags.Flags;
 import com.android.settings.testutils.shadow.ShadowAlertDialogCompat;
 import com.android.settings.testutils.shadow.ShadowAudioManager;
 import com.android.settings.testutils.shadow.ShadowBluetoothAdapter;
@@ -51,9 +61,12 @@
 import com.android.settingslib.bluetooth.CachedBluetoothDevice;
 import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager;
 import com.android.settingslib.bluetooth.HearingAidInfo;
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
 import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Answers;
@@ -63,42 +76,46 @@
 import org.robolectric.RobolectricTestRunner;
 import org.robolectric.RuntimeEnvironment;
 import org.robolectric.annotation.Config;
+import org.robolectric.shadow.api.Shadow;
+
+import java.util.concurrent.Executor;
 
 /** Tests for {@link AvailableMediaDeviceGroupController}. */
 @RunWith(RobolectricTestRunner.class)
-@Config(shadows = {
-        ShadowAudioManager.class,
-        ShadowBluetoothAdapter.class,
-        ShadowBluetoothUtils.class,
-        ShadowAlertDialogCompat.class,
-})
+@Config(
+        shadows = {
+            ShadowAudioManager.class,
+            ShadowBluetoothAdapter.class,
+            ShadowBluetoothUtils.class,
+            ShadowAlertDialogCompat.class,
+        })
 public class AvailableMediaDeviceGroupControllerTest {
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
 
     private static final String TEST_DEVICE_ADDRESS = "00:A1:A1:A1:A1:A1";
     private static final String PREFERENCE_KEY_1 = "pref_key_1";
 
-    @Mock
-    private AvailableMediaBluetoothDeviceUpdater mAvailableMediaBluetoothDeviceUpdater;
-    @Mock
-    private PreferenceScreen mPreferenceScreen;
+    @Mock private AvailableMediaBluetoothDeviceUpdater mAvailableMediaBluetoothDeviceUpdater;
+    @Mock private PreferenceScreen mPreferenceScreen;
+
     @Mock(answer = Answers.RETURNS_DEEP_STUBS)
     private PreferenceManager mPreferenceManager;
-    @Mock
-    private PackageManager mPackageManager;
-    @Mock
-    private BluetoothEventManager mEventManager;
-    @Mock
-    private LocalBluetoothManager mLocalBluetoothManager;
-    @Mock
-    private CachedBluetoothDeviceManager mCachedDeviceManager;
-    @Mock
-    private CachedBluetoothDevice mCachedBluetoothDevice;
+
+    @Mock private PackageManager mPackageManager;
+    @Mock private BluetoothEventManager mEventManager;
+    @Mock private LocalBluetoothManager mLocalBluetoothManager;
+    @Mock private LocalBluetoothProfileManager mLocalBtProfileManager;
+    @Mock private CachedBluetoothDeviceManager mCachedDeviceManager;
+    @Mock private LocalBluetoothLeBroadcastAssistant mAssistant;
+    @Mock private CachedBluetoothDevice mCachedBluetoothDevice;
 
     private PreferenceGroup mPreferenceGroup;
     private Context mContext;
     private Preference mPreference;
     private AvailableMediaDeviceGroupController mAvailableMediaDeviceGroupController;
     private AudioManager mAudioManager;
+    private ShadowBluetoothAdapter mShadowBluetoothAdapter;
 
     @Before
     public void setUp() {
@@ -113,19 +130,26 @@
         doReturn(mPackageManager).when(mContext).getPackageManager();
         doReturn(true).when(mPackageManager).hasSystemFeature(PackageManager.FEATURE_BLUETOOTH);
 
+        mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter());
+        mShadowBluetoothAdapter.setEnabled(true);
+        mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
+                BluetoothStatusCodes.FEATURE_NOT_SUPPORTED);
+        mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
+                BluetoothStatusCodes.FEATURE_NOT_SUPPORTED);
         ShadowBluetoothUtils.sLocalBluetoothManager = mLocalBluetoothManager;
         mLocalBluetoothManager = Utils.getLocalBtManager(mContext);
         mAudioManager = mContext.getSystemService(AudioManager.class);
         doReturn(mEventManager).when(mLocalBluetoothManager).getEventManager();
+        when(mLocalBluetoothManager.getProfileManager()).thenReturn(mLocalBtProfileManager);
         when(mLocalBluetoothManager.getCachedDeviceManager()).thenReturn(mCachedDeviceManager);
-        when(mCachedDeviceManager.findDevice(any(BluetoothDevice.class))).thenReturn(
-                mCachedBluetoothDevice);
+        when(mCachedDeviceManager.findDevice(any(BluetoothDevice.class)))
+                .thenReturn(mCachedBluetoothDevice);
         when(mCachedBluetoothDevice.getAddress()).thenReturn(TEST_DEVICE_ADDRESS);
 
-        mAvailableMediaDeviceGroupController = spy(
-                new AvailableMediaDeviceGroupController(mContext));
-        mAvailableMediaDeviceGroupController.
-                setBluetoothDeviceUpdater(mAvailableMediaBluetoothDeviceUpdater);
+        mAvailableMediaDeviceGroupController =
+                spy(new AvailableMediaDeviceGroupController(mContext));
+        mAvailableMediaDeviceGroupController.setBluetoothDeviceUpdater(
+                mAvailableMediaBluetoothDeviceUpdater);
         mAvailableMediaDeviceGroupController.setFragmentManager(
                 mActivity.getSupportFragmentManager());
         mAvailableMediaDeviceGroupController.mPreferenceGroup = mPreferenceGroup;
@@ -176,34 +200,55 @@
         mAvailableMediaDeviceGroupController.onStart();
 
         verify(mAvailableMediaBluetoothDeviceUpdater).registerCallback();
-        verify(mLocalBluetoothManager.getEventManager()).registerCallback(
-                any(BluetoothCallback.class));
+        verify(mLocalBluetoothManager.getEventManager())
+                .registerCallback(any(BluetoothCallback.class));
         verify(mAvailableMediaBluetoothDeviceUpdater).refreshPreference();
     }
 
     @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_LE_AUDIO_SHARING)
+    public void testRegister_audioSharingOn() {
+        setUpBroadcast();
+        // register the callback in onStart()
+        mAvailableMediaDeviceGroupController.onStart();
+        verify(mAssistant)
+                .registerServiceCallBack(
+                        any(Executor.class), any(BluetoothLeBroadcastAssistant.Callback.class));
+    }
+
+    @Test
     public void testUnregister() {
         // unregister the callback in onStop()
         mAvailableMediaDeviceGroupController.onStop();
         verify(mAvailableMediaBluetoothDeviceUpdater).unregisterCallback();
-        verify(mLocalBluetoothManager.getEventManager()).unregisterCallback(
-                any(BluetoothCallback.class));
+        verify(mLocalBluetoothManager.getEventManager())
+                .unregisterCallback(any(BluetoothCallback.class));
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_LE_AUDIO_SHARING)
+    public void testUnregister_audioSharingOn() {
+        setUpBroadcast();
+        // unregister the callback in onStop()
+        mAvailableMediaDeviceGroupController.onStop();
+        verify(mAssistant)
+                .unregisterServiceCallBack(any(BluetoothLeBroadcastAssistant.Callback.class));
     }
 
     @Test
     public void testGetAvailabilityStatus_noBluetoothFeature_returnUnSupported() {
         doReturn(false).when(mPackageManager).hasSystemFeature(PackageManager.FEATURE_BLUETOOTH);
 
-        assertThat(mAvailableMediaDeviceGroupController.getAvailabilityStatus()).isEqualTo(
-                UNSUPPORTED_ON_DEVICE);
+        assertThat(mAvailableMediaDeviceGroupController.getAvailabilityStatus())
+                .isEqualTo(UNSUPPORTED_ON_DEVICE);
     }
 
     @Test
     public void testGetAvailabilityStatus_BluetoothFeature_returnSupported() {
         doReturn(true).when(mPackageManager).hasSystemFeature(PackageManager.FEATURE_BLUETOOTH);
 
-        assertThat(mAvailableMediaDeviceGroupController.getAvailabilityStatus()).isEqualTo(
-                AVAILABLE_UNSEARCHABLE);
+        assertThat(mAvailableMediaDeviceGroupController.getAvailabilityStatus())
+                .isEqualTo(AVAILABLE_UNSEARCHABLE);
     }
 
     @Test
@@ -211,8 +256,8 @@
         mAudioManager.setMode(AudioManager.MODE_IN_CALL);
         mAvailableMediaDeviceGroupController.onAudioModeChanged();
 
-        assertThat(mPreferenceGroup.getTitle()).isEqualTo(
-                mContext.getText(R.string.connected_device_call_device_title));
+        assertThat(mPreferenceGroup.getTitle())
+                .isEqualTo(mContext.getText(R.string.connected_device_call_device_title));
     }
 
     @Test
@@ -220,8 +265,8 @@
         mAudioManager.setMode(AudioManager.MODE_NORMAL);
         mAvailableMediaDeviceGroupController.onAudioModeChanged();
 
-        assertThat(mPreferenceGroup.getTitle()).isEqualTo(
-                mContext.getText(R.string.connected_device_media_device_title));
+        assertThat(mPreferenceGroup.getTitle())
+                .isEqualTo(mContext.getText(R.string.connected_device_media_device_title));
     }
 
     @Test
@@ -243,16 +288,31 @@
     @Test
     public void onActiveDeviceChanged_hearingAidProfile_launchHearingAidPairingDialog() {
         when(mCachedBluetoothDevice.isConnectedAshaHearingAidDevice()).thenReturn(true);
-        when(mCachedBluetoothDevice.getDeviceMode()).thenReturn(
-                HearingAidInfo.DeviceMode.MODE_BINAURAL);
-        when(mCachedBluetoothDevice.getDeviceSide()).thenReturn(
-                HearingAidInfo.DeviceSide.SIDE_LEFT);
+        when(mCachedBluetoothDevice.getDeviceMode())
+                .thenReturn(HearingAidInfo.DeviceMode.MODE_BINAURAL);
+        when(mCachedBluetoothDevice.getDeviceSide())
+                .thenReturn(HearingAidInfo.DeviceSide.SIDE_LEFT);
 
-        mAvailableMediaDeviceGroupController.onActiveDeviceChanged(mCachedBluetoothDevice,
-                BluetoothProfile.HEARING_AID);
+        mAvailableMediaDeviceGroupController.onActiveDeviceChanged(
+                mCachedBluetoothDevice, BluetoothProfile.HEARING_AID);
         shadowMainLooper().idle();
 
         final AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
         assertThat(dialog.isShowing()).isTrue();
     }
+
+    private void setUpBroadcast() {
+        mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
+                BluetoothStatusCodes.FEATURE_SUPPORTED);
+        mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
+                BluetoothStatusCodes.FEATURE_SUPPORTED);
+        when(mLocalBtProfileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(mAssistant);
+        doNothing()
+                .when(mAssistant)
+                .registerServiceCallBack(
+                        any(Executor.class), any(BluetoothLeBroadcastAssistant.Callback.class));
+        doNothing()
+                .when(mAssistant)
+                .unregisterServiceCallBack(any(BluetoothLeBroadcastAssistant.Callback.class));
+    }
 }
diff --git a/tests/robotests/src/com/android/settings/dashboard/profileselector/ProfileSelectFragmentTest.java b/tests/robotests/src/com/android/settings/dashboard/profileselector/ProfileSelectFragmentTest.java
index 3df6449..17e0d1c 100644
--- a/tests/robotests/src/com/android/settings/dashboard/profileselector/ProfileSelectFragmentTest.java
+++ b/tests/robotests/src/com/android/settings/dashboard/profileselector/ProfileSelectFragmentTest.java
@@ -39,17 +39,24 @@
 import android.platform.test.flag.junit.SetFlagsRule;
 import android.util.ArraySet;
 
+import androidx.annotation.NonNull;
 import androidx.fragment.app.Fragment;
 import androidx.fragment.app.FragmentActivity;
+import androidx.fragment.app.FragmentManager;
+import androidx.fragment.app.FragmentTransaction;
+import androidx.lifecycle.Lifecycle;
+import androidx.viewpager2.widget.ViewPager2;
 
 import com.android.settings.SettingsActivity;
 import com.android.settings.SettingsPreferenceFragmentTest;
+import com.android.settings.dashboard.profileselector.ProfileSelectFragment.ViewPagerAdapter;
 import com.android.settings.testutils.shadow.ShadowUserManager;
 
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 import org.robolectric.Robolectric;
 import org.robolectric.RobolectricTestRunner;
@@ -75,6 +82,9 @@
     private TestProfileSelectFragment mFragment;
     private FragmentActivity mActivity;
     private ShadowUserManager mUserManager;
+    @Mock private FragmentManager mFragmentManager;
+    @Mock private Lifecycle mLifecycle;
+    @Mock private FragmentTransaction mFragmentTransaction;
     @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
 
     @Before
@@ -97,6 +107,16 @@
     public void getTabId_setArgumentWork_setCorrectTab() {
         final Bundle bundle = new Bundle();
         bundle.putInt(SettingsActivity.EXTRA_SHOW_FRAGMENT_TAB, WORK_TAB);
+        ViewPager2 viewPager = new ViewPager2(mContext);
+        TestProfileSelectFragment profileSelectFragment = new TestProfileSelectFragment();
+        ViewPagerAdapter viewPagerAdapter =
+                new TestViewPagerAdapter(mFragmentManager, mLifecycle, profileSelectFragment);
+
+        when(mFragmentManager.beginTransaction()).thenReturn(mFragmentTransaction);
+        viewPager.setAdapter(viewPagerAdapter);
+        mFragment.setViewPager(viewPager);
+        profileSelectFragment.setViewPager(viewPager);
+        mFragmentManager.beginTransaction().add(profileSelectFragment, "tag");
 
         assertThat(mFragment.getTabId(mActivity, bundle)).isEqualTo(WORK_TAB);
     }
@@ -105,6 +125,16 @@
     public void getTabId_setArgumentPrivate_setCorrectTab() {
         final Bundle bundle = new Bundle();
         bundle.putInt(SettingsActivity.EXTRA_SHOW_FRAGMENT_TAB, PRIVATE_TAB);
+        ViewPager2 viewPager = new ViewPager2(mContext);
+        TestProfileSelectFragment profileSelectFragment = new TestProfileSelectFragment();
+        ViewPagerAdapter viewPagerAdapter =
+                new TestViewPagerAdapter(mFragmentManager, mLifecycle, profileSelectFragment);
+
+        when(mFragmentManager.beginTransaction()).thenReturn(mFragmentTransaction);
+        viewPager.setAdapter(viewPagerAdapter);
+        mFragment.setViewPager(viewPager);
+        profileSelectFragment.setViewPager(viewPager);
+        mFragmentManager.beginTransaction().add(profileSelectFragment, "tag");
 
         assertThat(mFragment.getTabId(mActivity, bundle)).isEqualTo(PRIVATE_TAB);
     }
@@ -113,6 +143,16 @@
     public void getTabId_setArgumentPersonal_setCorrectTab() {
         final Bundle bundle = new Bundle();
         bundle.putInt(SettingsActivity.EXTRA_SHOW_FRAGMENT_TAB, PERSONAL_TAB);
+        ViewPager2 viewPager = new ViewPager2(mContext);
+        TestProfileSelectFragment profileSelectFragment = new TestProfileSelectFragment();
+        ViewPagerAdapter viewPagerAdapter =
+                new TestViewPagerAdapter(mFragmentManager, mLifecycle, profileSelectFragment);
+
+        when(mFragmentManager.beginTransaction()).thenReturn(mFragmentTransaction);
+        viewPager.setAdapter(viewPagerAdapter);
+        mFragment.setViewPager(viewPager);
+        profileSelectFragment.setViewPager(viewPager);
+        mFragmentManager.beginTransaction().add(profileSelectFragment, "tag");
 
         assertThat(mFragment.getTabId(mActivity, bundle)).isEqualTo(PERSONAL_TAB);
     }
@@ -310,4 +350,18 @@
             };
         }
     }
+
+    static class TestViewPagerAdapter extends ViewPagerAdapter {
+        TestViewPagerAdapter(
+                @NonNull FragmentManager fragmentManager,
+                @NonNull Lifecycle lifecycle,
+                ProfileSelectFragment profileSelectFragment) {
+            super(fragmentManager, lifecycle, profileSelectFragment);
+        }
+
+        @Override
+        int getTabForPosition(int position) {
+            return position;
+        }
+    }
 }
diff --git a/tests/unit/src/com/android/settings/privatespace/PrivateSpaceMaintainerTest.java b/tests/unit/src/com/android/settings/privatespace/PrivateSpaceMaintainerTest.java
index 1d27326..1605ae6 100644
--- a/tests/unit/src/com/android/settings/privatespace/PrivateSpaceMaintainerTest.java
+++ b/tests/unit/src/com/android/settings/privatespace/PrivateSpaceMaintainerTest.java
@@ -198,4 +198,40 @@
         assertThat(privateSpaceMaintainer.doesPrivateSpaceExist()).isFalse();
         assertThat(privateSpaceMaintainer.lockPrivateSpace()).isFalse();
     }
+
+    /**
+     * Tests that {@link PrivateSpaceMaintainer#createPrivateSpace()} when no PS exists sets
+     * USER_SETUP_COMPLETE setting.
+     */
+    @Test
+    public void createPrivateSpace_psDoesNotExist_setsUserSetupComplete() {
+        PrivateSpaceMaintainer privateSpaceMaintainer =
+                PrivateSpaceMaintainer.getInstance(mContext);
+        privateSpaceMaintainer.createPrivateSpace();
+        assertThat(getSecureUserSetupComplete()).isEqualTo(1);
+    }
+
+    /**
+     * Tests that {@link PrivateSpaceMaintainer#createPrivateSpace()} when PS exists does not
+     * change USER_SETUP_COMPLETE setting.
+     */
+    @Test
+    public void createPrivateSpace_pSExists_doesNotChangeUserSetupSetting() {
+        PrivateSpaceMaintainer privateSpaceMaintainer =
+                PrivateSpaceMaintainer.getInstance(mContext);
+        privateSpaceMaintainer.createPrivateSpace();
+        assertThat(getSecureUserSetupComplete()).isEqualTo(1);
+        privateSpaceMaintainer.createPrivateSpace();
+        assertThat(getSecureUserSetupComplete()).isEqualTo(1);
+    }
+
+    private int getSecureUserSetupComplete() {
+        PrivateSpaceMaintainer privateSpaceMaintainer =
+                PrivateSpaceMaintainer.getInstance(mContext);
+        return Settings.Secure.getIntForUser(
+                mContentResolver,
+                Settings.Secure.USER_SETUP_COMPLETE,
+                0,
+                privateSpaceMaintainer.getPrivateProfileHandle().getIdentifier());
+    }
 }