Merge "[Audiosharing] Set visibility by active device." into main
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/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(