[Audiosharing] Add button action in detail page.

Bug: 308368124
Test: manual
Change-Id: I44e631cb75af432965d2221e71676146ea1537b6
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamButtonController.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamButtonController.java
index bb729d6..47597cf 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamButtonController.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamButtonController.java
@@ -16,39 +16,170 @@
 
 package com.android.settings.connecteddevice.audiosharing.audiostreams;
 
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothLeBroadcastAssistant;
+import android.bluetooth.BluetoothLeBroadcastMetadata;
+import android.bluetooth.BluetoothLeBroadcastReceiveState;
 import android.content.Context;
+import android.util.Log;
+import android.view.View;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.lifecycle.DefaultLifecycleObserver;
+import androidx.lifecycle.LifecycleOwner;
 import androidx.preference.PreferenceScreen;
 
 import com.android.settings.R;
+import com.android.settings.bluetooth.Utils;
 import com.android.settings.core.BasePreferenceController;
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
+import com.android.settingslib.utils.ThreadUtils;
 import com.android.settingslib.widget.ActionButtonsPreference;
 
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
 public class AudioStreamButtonController extends BasePreferenceController
         implements DefaultLifecycleObserver {
+    private static final String TAG = "AudioStreamButtonController";
     private static final String KEY = "audio_stream_button";
+    private final BluetoothLeBroadcastAssistant.Callback mBroadcastAssistantCallback =
+            new AudioStreamsBroadcastAssistantCallback() {
+                @Override
+                public void onSourceRemoved(BluetoothDevice sink, int sourceId, int reason) {
+                    super.onSourceRemoved(sink, sourceId, reason);
+                    updateButton();
+                }
+
+                @Override
+                public void onSourceRemoveFailed(BluetoothDevice sink, int sourceId, int reason) {
+                    super.onSourceRemoveFailed(sink, sourceId, reason);
+                    updateButton();
+                }
+
+                @Override
+                public void onReceiveStateChanged(
+                        BluetoothDevice sink,
+                        int sourceId,
+                        BluetoothLeBroadcastReceiveState state) {
+                    super.onReceiveStateChanged(sink, sourceId, state);
+                    if (mAudioStreamsHelper.isConnected(state)) {
+                        updateButton();
+                    }
+                }
+
+                @Override
+                public void onSourceAddFailed(
+                        BluetoothDevice sink, BluetoothLeBroadcastMetadata source, int reason) {
+                    super.onSourceAddFailed(sink, source, reason);
+                    updateButton();
+                }
+
+                @Override
+                public void onSourceLost(int broadcastId) {
+                    super.onSourceLost(broadcastId);
+                    updateButton();
+                }
+            };
+
+    private final AudioStreamsRepository mAudioStreamsRepository =
+            AudioStreamsRepository.getInstance();
+    private final Executor mExecutor;
+    private final AudioStreamsHelper mAudioStreamsHelper;
+    private final @Nullable LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant;
     private @Nullable ActionButtonsPreference mPreference;
     private int mBroadcastId = -1;
 
     public AudioStreamButtonController(Context context, String preferenceKey) {
         super(context, preferenceKey);
+        mExecutor = Executors.newSingleThreadExecutor();
+        mAudioStreamsHelper = new AudioStreamsHelper(Utils.getLocalBtManager(context));
+        mLeBroadcastAssistant = mAudioStreamsHelper.getLeBroadcastAssistant();
+    }
+
+    @Override
+    public void onStart(@NonNull LifecycleOwner owner) {
+        if (mLeBroadcastAssistant == null) {
+            Log.w(TAG, "onStart(): LeBroadcastAssistant is null!");
+            return;
+        }
+        mLeBroadcastAssistant.registerServiceCallBack(mExecutor, mBroadcastAssistantCallback);
+    }
+
+    @Override
+    public void onStop(@NonNull LifecycleOwner owner) {
+        if (mLeBroadcastAssistant == null) {
+            Log.w(TAG, "onStop(): LeBroadcastAssistant is null!");
+            return;
+        }
+        mLeBroadcastAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback);
     }
 
     @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);
-        }
+        updateButton();
         super.displayPreference(screen);
     }
 
+    private void updateButton() {
+        if (mPreference != null) {
+            if (mAudioStreamsHelper.getAllConnectedSources().stream()
+                    .map(BluetoothLeBroadcastReceiveState::getBroadcastId)
+                    .anyMatch(connectedBroadcastId -> connectedBroadcastId == mBroadcastId)) {
+                ThreadUtils.postOnMainThread(
+                        () -> {
+                            if (mPreference != null) {
+                                mPreference.setButton1Enabled(true);
+                                mPreference
+                                        .setButton1Text(
+                                                R.string.bluetooth_device_context_disconnect)
+                                        .setButton1Icon(R.drawable.ic_settings_close)
+                                        .setButton1OnClickListener(
+                                                unused -> {
+                                                    if (mPreference != null) {
+                                                        mPreference.setButton1Enabled(false);
+                                                    }
+                                                    mAudioStreamsHelper.removeSource(mBroadcastId);
+                                                });
+                            }
+                        });
+            } else {
+                View.OnClickListener clickToRejoin =
+                        unused ->
+                                ThreadUtils.postOnBackgroundThread(
+                                        () -> {
+                                            var metadata =
+                                                    mAudioStreamsRepository.getSavedMetadata(
+                                                            mContext, mBroadcastId);
+                                            if (metadata != null) {
+                                                mAudioStreamsHelper.addSource(metadata);
+                                                ThreadUtils.postOnMainThread(
+                                                        () -> {
+                                                            if (mPreference != null) {
+                                                                mPreference.setButton1Enabled(
+                                                                        false);
+                                                            }
+                                                        });
+                                            }
+                                        });
+                ThreadUtils.postOnMainThread(
+                        () -> {
+                            if (mPreference != null) {
+                                mPreference.setButton1Enabled(true);
+                                mPreference
+                                        .setButton1Text(R.string.bluetooth_device_context_connect)
+                                        .setButton1Icon(R.drawable.ic_add_24dp)
+                                        .setButton1OnClickListener(clickToRejoin);
+                            }
+                        });
+            }
+        } else {
+            Log.w(TAG, "updateButton(): preference is null!");
+        }
+    }
+
     @Override
     public int getAvailabilityStatus() {
         return AVAILABLE;
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamHeaderController.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamHeaderController.java
index 89f24bc..3524543 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamHeaderController.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamHeaderController.java
@@ -16,22 +16,64 @@
 
 package com.android.settings.connecteddevice.audiosharing.audiostreams;
 
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothLeBroadcastAssistant;
+import android.bluetooth.BluetoothLeBroadcastReceiveState;
 import android.content.Context;
+import android.util.Log;
 
+import androidx.annotation.NonNull;
 import androidx.lifecycle.DefaultLifecycleObserver;
+import androidx.lifecycle.LifecycleOwner;
 import androidx.preference.PreferenceScreen;
 
 import com.android.settings.R;
+import com.android.settings.bluetooth.Utils;
 import com.android.settings.core.BasePreferenceController;
 import com.android.settings.dashboard.DashboardFragment;
 import com.android.settings.widget.EntityHeaderController;
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
+import com.android.settingslib.utils.ThreadUtils;
 import com.android.settingslib.widget.LayoutPreference;
 
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
 import javax.annotation.Nullable;
 
 public class AudioStreamHeaderController extends BasePreferenceController
         implements DefaultLifecycleObserver {
+    private static final String TAG = "AudioStreamHeaderController";
     private static final String KEY = "audio_stream_header";
+    private final Executor mExecutor;
+    private final AudioStreamsHelper mAudioStreamsHelper;
+    @Nullable private final LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant;
+    private final BluetoothLeBroadcastAssistant.Callback mBroadcastAssistantCallback =
+            new AudioStreamsBroadcastAssistantCallback() {
+                @Override
+                public void onSourceRemoved(BluetoothDevice sink, int sourceId, int reason) {
+                    super.onSourceRemoved(sink, sourceId, reason);
+                    updateSummary();
+                }
+
+                @Override
+                public void onSourceLost(int broadcastId) {
+                    super.onSourceLost(broadcastId);
+                    updateSummary();
+                }
+
+                @Override
+                public void onReceiveStateChanged(
+                        BluetoothDevice sink,
+                        int sourceId,
+                        BluetoothLeBroadcastReceiveState state) {
+                    super.onReceiveStateChanged(sink, sourceId, state);
+                    if (mAudioStreamsHelper.isConnected(state)) {
+                        updateSummary();
+                    }
+                }
+            };
+
     private @Nullable EntityHeaderController mHeaderController;
     private @Nullable DashboardFragment mFragment;
     private String mBroadcastName = "";
@@ -39,6 +81,27 @@
 
     public AudioStreamHeaderController(Context context, String preferenceKey) {
         super(context, preferenceKey);
+        mExecutor = Executors.newSingleThreadExecutor();
+        mAudioStreamsHelper = new AudioStreamsHelper(Utils.getLocalBtManager(context));
+        mLeBroadcastAssistant = mAudioStreamsHelper.getLeBroadcastAssistant();
+    }
+
+    @Override
+    public void onStart(@NonNull LifecycleOwner owner) {
+        if (mLeBroadcastAssistant == null) {
+            Log.w(TAG, "onStart(): LeBroadcastAssistant is null!");
+            return;
+        }
+        mLeBroadcastAssistant.registerServiceCallBack(mExecutor, mBroadcastAssistantCallback);
+    }
+
+    @Override
+    public void onStop(@NonNull LifecycleOwner owner) {
+        if (mLeBroadcastAssistant == null) {
+            Log.w(TAG, "onStop(): LeBroadcastAssistant is null!");
+            return;
+        }
+        mLeBroadcastAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback);
     }
 
     @Override
@@ -55,14 +118,37 @@
             }
             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);
+            updateSummary();
         }
         super.displayPreference(screen);
     }
 
+    private void updateSummary() {
+        var unused =
+                ThreadUtils.postOnBackgroundThread(
+                        () -> {
+                            var latestSummary =
+                                    mAudioStreamsHelper.getAllConnectedSources().stream()
+                                                    .map(
+                                                            BluetoothLeBroadcastReceiveState
+                                                                    ::getBroadcastId)
+                                                    .anyMatch(
+                                                            connectedBroadcastId ->
+                                                                    connectedBroadcastId
+                                                                            == mBroadcastId)
+                                            ? "Listening now"
+                                            : "";
+                            ThreadUtils.postOnMainThread(
+                                    () -> {
+                                        if (mHeaderController != null) {
+                                            mHeaderController.setSummary(latestSummary);
+                                            mHeaderController.done(true);
+                                        }
+                                    });
+                        });
+    }
+
     @Override
     public int getAvailabilityStatus() {
         return AVAILABLE;
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsBroadcastAssistantCallback.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsBroadcastAssistantCallback.java
index 84e753c..9fb5b21 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsBroadcastAssistantCallback.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsBroadcastAssistantCallback.java
@@ -24,21 +24,12 @@
 
 import com.android.settingslib.bluetooth.BluetoothUtils;
 
-import java.util.Locale;
-
 public class AudioStreamsBroadcastAssistantCallback
         implements BluetoothLeBroadcastAssistant.Callback {
 
     private static final String TAG = "AudioStreamsBroadcastAssistantCallback";
     private static final boolean DEBUG = BluetoothUtils.D;
 
-    private final AudioStreamsProgressCategoryController mCategoryController;
-
-    public AudioStreamsBroadcastAssistantCallback(
-            AudioStreamsProgressCategoryController audioStreamsProgressCategoryController) {
-        mCategoryController = audioStreamsProgressCategoryController;
-    }
-
     @Override
     public void onReceiveStateChanged(
             BluetoothDevice sink, int sourceId, BluetoothLeBroadcastReceiveState state) {
@@ -52,45 +43,30 @@
                             + " state: "
                             + state);
         }
-        mCategoryController.handleSourceConnected(state);
     }
 
     @Override
     public void onSearchStartFailed(int reason) {
         Log.w(TAG, "onSearchStartFailed() reason : " + reason);
-        mCategoryController.showToast(
-                String.format(Locale.US, "Failed to start scanning, reason %d", reason));
     }
 
     @Override
     public void onSearchStarted(int reason) {
-        if (mCategoryController == null) {
-            Log.w(TAG, "onSearchStarted() : mCategoryController is null!");
-            return;
-        }
         if (DEBUG) {
             Log.d(TAG, "onSearchStarted() reason : " + reason);
         }
-        mCategoryController.setScanning(true);
     }
 
     @Override
     public void onSearchStopFailed(int reason) {
         Log.w(TAG, "onSearchStopFailed() reason : " + reason);
-        mCategoryController.showToast(
-                String.format(Locale.US, "Failed to stop scanning, reason %d", reason));
     }
 
     @Override
     public void onSearchStopped(int reason) {
-        if (mCategoryController == null) {
-            Log.w(TAG, "onSearchStopped() : mCategoryController is null!");
-            return;
-        }
         if (DEBUG) {
             Log.d(TAG, "onSearchStopped() reason : " + reason);
         }
-        mCategoryController.setScanning(false);
     }
 
     @Override
@@ -106,8 +82,6 @@
                             + " reason: "
                             + reason);
         }
-        mCategoryController.showToast(
-                String.format(Locale.US, "Failed to join broadcast, reason %d", reason));
     }
 
     @Override
@@ -126,14 +100,9 @@
 
     @Override
     public void onSourceFound(BluetoothLeBroadcastMetadata source) {
-        if (mCategoryController == null) {
-            Log.w(TAG, "onSourceFound() : mCategoryController is null!");
-            return;
-        }
         if (DEBUG) {
             Log.d(TAG, "onSourceFound() broadcastId : " + source.getBroadcastId());
         }
-        mCategoryController.handleSourceFound(source);
     }
 
     @Override
@@ -141,7 +110,6 @@
         if (DEBUG) {
             Log.d(TAG, "onSourceLost() broadcastId : " + broadcastId);
         }
-        mCategoryController.handleSourceLost(broadcastId);
     }
 
     @Override
@@ -153,12 +121,6 @@
     @Override
     public void onSourceRemoveFailed(BluetoothDevice sink, int sourceId, int reason) {
         Log.w(TAG, "onSourceRemoveFailed() sourceId : " + sourceId + " reason : " + reason);
-        mCategoryController.showToast(
-                String.format(
-                        Locale.US,
-                        "Failed to remove source %d for sink %s",
-                        sourceId,
-                        sink.getAddress()));
     }
 
     @Override
@@ -166,8 +128,5 @@
         if (DEBUG) {
             Log.d(TAG, "onSourceRemoved() sourceId : " + sourceId + " reason : " + reason);
         }
-        mCategoryController.showToast(
-                String.format(
-                        Locale.US, "Source %d removed for sink %s", sourceId, sink.getAddress()));
     }
 }
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryCallback.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryCallback.java
new file mode 100644
index 0000000..15a0603
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryCallback.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2024 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.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothLeBroadcastMetadata;
+import android.bluetooth.BluetoothLeBroadcastReceiveState;
+import android.util.Log;
+
+import java.util.Locale;
+
+public class AudioStreamsProgressCategoryCallback extends AudioStreamsBroadcastAssistantCallback {
+    private static final String TAG = "AudioStreamsProgressCategoryCallback";
+
+    private final AudioStreamsProgressCategoryController mCategoryController;
+
+    public AudioStreamsProgressCategoryCallback(
+            AudioStreamsProgressCategoryController audioStreamsProgressCategoryController) {
+        mCategoryController = audioStreamsProgressCategoryController;
+    }
+
+    @Override
+    public void onReceiveStateChanged(
+            BluetoothDevice sink, int sourceId, BluetoothLeBroadcastReceiveState state) {
+        super.onReceiveStateChanged(sink, sourceId, state);
+        mCategoryController.handleSourceConnected(state);
+    }
+
+    @Override
+    public void onSearchStartFailed(int reason) {
+        super.onSearchStartFailed(reason);
+        mCategoryController.showToast(
+                String.format(Locale.US, "Failed to start scanning, reason %d", reason));
+    }
+
+    @Override
+    public void onSearchStarted(int reason) {
+        super.onSearchStarted(reason);
+        if (mCategoryController == null) {
+            Log.w(TAG, "onSearchStarted() : mCategoryController is null!");
+            return;
+        }
+        mCategoryController.setScanning(true);
+    }
+
+    @Override
+    public void onSearchStopFailed(int reason) {
+        super.onSearchStopFailed(reason);
+        mCategoryController.showToast(
+                String.format(Locale.US, "Failed to stop scanning, reason %d", reason));
+    }
+
+    @Override
+    public void onSearchStopped(int reason) {
+        super.onSearchStopped(reason);
+        if (mCategoryController == null) {
+            Log.w(TAG, "onSearchStopped() : mCategoryController is null!");
+            return;
+        }
+        mCategoryController.setScanning(false);
+    }
+
+    @Override
+    public void onSourceAddFailed(
+            BluetoothDevice sink, BluetoothLeBroadcastMetadata source, int reason) {
+        super.onSourceAddFailed(sink, source, reason);
+        mCategoryController.showToast(
+                String.format(Locale.US, "Failed to join broadcast, reason %d", reason));
+    }
+
+    @Override
+    public void onSourceFound(BluetoothLeBroadcastMetadata source) {
+        super.onSourceFound(source);
+        if (mCategoryController == null) {
+            Log.w(TAG, "onSourceFound() : mCategoryController is null!");
+            return;
+        }
+        mCategoryController.handleSourceFound(source);
+    }
+
+    @Override
+    public void onSourceLost(int broadcastId) {
+        super.onSourceLost(broadcastId);
+        mCategoryController.handleSourceLost(broadcastId);
+    }
+
+    @Override
+    public void onSourceRemoveFailed(BluetoothDevice sink, int sourceId, int reason) {
+        super.onSourceRemoveFailed(sink, sourceId, reason);
+        mCategoryController.showToast(
+                String.format(
+                        Locale.US,
+                        "Failed to remove source %d for sink %s",
+                        sourceId,
+                        sink.getAddress()));
+    }
+
+    @Override
+    public void onSourceRemoved(BluetoothDevice sink, int sourceId, int reason) {
+        super.onSourceRemoved(sink, sourceId, reason);
+        mCategoryController.showToast(
+                String.format(
+                        Locale.US, "Source %d removed for sink %s", sourceId, sink.getAddress()));
+    }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java
index cb9975d..b3b0743 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java
@@ -74,6 +74,9 @@
                 }
             };
 
+    private final AudioStreamsRepository mAudioStreamsRepository =
+            AudioStreamsRepository.getInstance();
+
     enum AudioStreamState {
         // When mTimedSourceFromQrCode is present and this source has not been synced.
         WAIT_FOR_SYNC,
@@ -86,7 +89,7 @@
     }
 
     private final Executor mExecutor;
-    private final AudioStreamsBroadcastAssistantCallback mBroadcastAssistantCallback;
+    private final AudioStreamsProgressCategoryCallback mBroadcastAssistantCallback;
     private final AudioStreamsHelper mAudioStreamsHelper;
     private final @Nullable LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant;
     private final @Nullable LocalBluetoothManager mBluetoothManager;
@@ -102,7 +105,7 @@
         mBluetoothManager = Utils.getLocalBtManager(mContext);
         mAudioStreamsHelper = new AudioStreamsHelper(mBluetoothManager);
         mLeBroadcastAssistant = mAudioStreamsHelper.getLeBroadcastAssistant();
-        mBroadcastAssistantCallback = new AudioStreamsBroadcastAssistantCallback(this);
+        mBroadcastAssistantCallback = new AudioStreamsProgressCategoryCallback(this);
     }
 
     @Override
@@ -170,6 +173,7 @@
                                                 source, (AudioStreamPreference) preference));
                     } else {
                         mAudioStreamsHelper.addSource(source);
+                        mAudioStreamsRepository.cacheMetadata(source);
                         ((AudioStreamPreference) preference)
                                 .setAudioStreamState(AudioStreamState.WAIT_FOR_SOURCE_ADD);
                         updatePreferenceConnectionState(
@@ -202,6 +206,7 @@
                             return v;
                         }
                         mAudioStreamsHelper.addSource(pendingSource);
+                        mAudioStreamsRepository.cacheMetadata(pendingSource);
                         mTimedSourceFromQrCode.consumed();
                         v.setAudioStreamState(AudioStreamState.WAIT_FOR_SOURCE_ADD);
                         updatePreferenceConnectionState(
@@ -236,6 +241,7 @@
                     var fromState = v.getAudioStreamState();
                     if (fromState == AudioStreamState.SYNCED) {
                         mAudioStreamsHelper.addSource(metadataFromQrCode);
+                        mAudioStreamsRepository.cacheMetadata(metadataFromQrCode);
                         mTimedSourceFromQrCode.consumed();
                         v.setAudioStreamState(AudioStreamState.WAIT_FOR_SOURCE_ADD);
                         updatePreferenceConnectionState(
@@ -302,6 +308,16 @@
                             v, sourceAddedState, p -> launchDetailFragment(broadcastIdConnected));
                     return v;
                 });
+        // Saved connected metadata for user to re-join this broadcast later.
+        var unused =
+                ThreadUtils.postOnBackgroundThread(
+                        () -> {
+                            var cached =
+                                    mAudioStreamsRepository.getCachedMetadata(broadcastIdConnected);
+                            if (cached != null) {
+                                mAudioStreamsRepository.saveMetadata(mContext, cached);
+                            }
+                        });
     }
 
     private static String getPreferenceSummary(AudioStreamState state) {
@@ -457,11 +473,13 @@
                                                                     R.id.broadcast_edit_text))
                                                     .getText()
                                                     .toString();
-                                    mAudioStreamsHelper.addSource(
+                                    var metadata =
                                             new BluetoothLeBroadcastMetadata.Builder(source)
                                                     .setBroadcastCode(
                                                             code.getBytes(StandardCharsets.UTF_8))
-                                                    .build());
+                                                    .build();
+                                    mAudioStreamsHelper.addSource(metadata);
+                                    mAudioStreamsRepository.cacheMetadata(metadata);
                                     preference.setAudioStreamState(
                                             AudioStreamState.WAIT_FOR_SOURCE_ADD);
                                     updatePreferenceConnectionState(
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsRepository.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsRepository.java
new file mode 100644
index 0000000..65245ac
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsRepository.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2024 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.bluetooth.BluetoothLeBroadcastMetadata;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.util.Log;
+
+import com.android.settingslib.bluetooth.BluetoothLeBroadcastMetadataExt;
+import com.android.settingslib.bluetooth.BluetoothUtils;
+import com.android.settingslib.utils.ThreadUtils;
+
+import java.util.concurrent.ConcurrentHashMap;
+
+import javax.annotation.Nullable;
+
+/** Manages the caching and storage of Bluetooth audio stream metadata. */
+public class AudioStreamsRepository {
+
+    private static final String TAG = "AudioStreamsRepository";
+    private static final boolean DEBUG = BluetoothUtils.D;
+
+    private static final String PREF_KEY = "bluetooth_audio_stream_pref";
+    private static final String METADATA_KEY = "bluetooth_audio_stream_metadata";
+
+    @Nullable
+    private static AudioStreamsRepository sInstance = null;
+
+    private AudioStreamsRepository() {}
+
+    /**
+     * Gets the single instance of AudioStreamsRepository.
+     *
+     * @return The AudioStreamsRepository instance.
+     */
+    public static synchronized AudioStreamsRepository getInstance() {
+        if (sInstance == null) {
+            sInstance = new AudioStreamsRepository();
+        }
+        return sInstance;
+    }
+
+    private final ConcurrentHashMap<Integer, BluetoothLeBroadcastMetadata>
+            mBroadcastIdToMetadataCacheMap = new ConcurrentHashMap<>();
+
+    /**
+     * Caches BluetoothLeBroadcastMetadata in a local cache.
+     *
+     * @param metadata The BluetoothLeBroadcastMetadata to be cached.
+     */
+    void cacheMetadata(BluetoothLeBroadcastMetadata metadata) {
+        if (DEBUG) {
+            Log.d(
+                    TAG,
+                    "cacheMetadata(): broadcastId "
+                            + metadata.getBroadcastId()
+                            + " saved in local cache.");
+        }
+        mBroadcastIdToMetadataCacheMap.put(metadata.getBroadcastId(), metadata);
+    }
+
+    /**
+     * Gets cached BluetoothLeBroadcastMetadata by broadcastId.
+     *
+     * @param broadcastId The broadcastId to look up in the cache.
+     * @return The cached BluetoothLeBroadcastMetadata or null if not found.
+     */
+    @Nullable
+    BluetoothLeBroadcastMetadata getCachedMetadata(int broadcastId) {
+        var metadata = mBroadcastIdToMetadataCacheMap.get(broadcastId);
+        if (metadata == null) {
+            Log.w(
+                    TAG,
+                    "getCachedMetadata(): broadcastId not found in"
+                            + " mBroadcastIdToMetadataCacheMap.");
+            return null;
+        }
+        return metadata;
+    }
+
+    /**
+     * Saves metadata to SharedPreferences asynchronously.
+     *
+     * @param context The context.
+     * @param metadata The BluetoothLeBroadcastMetadata to be saved.
+     */
+    void saveMetadata(Context context, BluetoothLeBroadcastMetadata metadata) {
+        var unused =
+                ThreadUtils.postOnBackgroundThread(
+                        () -> {
+                            SharedPreferences sharedPref =
+                                    context.getSharedPreferences(PREF_KEY, Context.MODE_PRIVATE);
+                            if (sharedPref != null) {
+                                SharedPreferences.Editor editor = sharedPref.edit();
+                                editor.putString(
+                                        METADATA_KEY,
+                                        BluetoothLeBroadcastMetadataExt.INSTANCE.toQrCodeString(
+                                                metadata));
+                                editor.apply();
+                                if (DEBUG) {
+                                    Log.d(
+                                            TAG,
+                                            "saveMetadata(): broadcastId "
+                                                    + metadata.getBroadcastId()
+                                                    + " metadata saved in storage.");
+                                }
+                            }
+                        });
+    }
+
+    /**
+     * Gets saved metadata from SharedPreferences.
+     *
+     * @param context The context.
+     * @param broadcastId The broadcastId to retrieve metadata for.
+     * @return The saved BluetoothLeBroadcastMetadata or null if not found.
+     */
+    @Nullable
+    BluetoothLeBroadcastMetadata getSavedMetadata(Context context, int broadcastId) {
+        SharedPreferences sharedPref = context.getSharedPreferences(PREF_KEY, Context.MODE_PRIVATE);
+        if (sharedPref != null) {
+            String savedMetadataStr = sharedPref.getString(METADATA_KEY, null);
+            if (savedMetadataStr == null) {
+                Log.w(TAG, "getSavedMetadata(): savedMetadataStr is null");
+                return null;
+            }
+            var savedMetadata =
+                    BluetoothLeBroadcastMetadataExt.INSTANCE.convertToBroadcastMetadata(
+                            savedMetadataStr);
+            if (savedMetadata == null || savedMetadata.getBroadcastId() != broadcastId) {
+                Log.w(TAG, "getSavedMetadata(): savedMetadata doesn't match broadcast Id.");
+                return null;
+            }
+            if (DEBUG) {
+                Log.d(
+                        TAG,
+                        "getSavedMetadata(): broadcastId "
+                                + savedMetadata.getBroadcastId()
+                                + " metadata found in storage.");
+            }
+            return savedMetadata;
+        }
+        return null;
+    }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsScanQrCodeController.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsScanQrCodeController.java
index 549e725..e006cec 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsScanQrCodeController.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsScanQrCodeController.java
@@ -16,7 +16,6 @@
 
 package com.android.settings.connecteddevice.audiosharing.audiostreams;
 
-import android.bluetooth.BluetoothLeBroadcastMetadata;
 import android.bluetooth.BluetoothProfile;
 import android.content.Context;
 import android.content.Intent;
@@ -124,10 +123,6 @@
                 });
     }
 
-    void addSource(BluetoothLeBroadcastMetadata source) {
-        mAudioStreamsHelper.addSource(source);
-    }
-
     private void updateVisibility() {
         ThreadUtils.postOnBackgroundThread(
                 () -> {