diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamPreference.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamPreference.java index 8f701a36857..ffb0b884fda 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamPreference.java +++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamPreference.java @@ -16,6 +16,9 @@ package com.android.settings.connecteddevice.audiosharing.audiostreams; +import android.bluetooth.BluetoothLeAudioContentMetadata; +import android.bluetooth.BluetoothLeBroadcastMetadata; +import android.bluetooth.BluetoothLeBroadcastReceiveState; import android.content.Context; import android.util.AttributeSet; @@ -24,11 +27,13 @@ import androidx.annotation.Nullable; import com.android.settings.R; import com.android.settingslib.widget.TwoTargetPreference; +import com.google.common.base.Strings; + /** * Custom preference class for managing audio stream preferences with an optional lock icon. Extends * {@link TwoTargetPreference}. */ -public class AudioStreamPreference extends TwoTargetPreference { +class AudioStreamPreference extends TwoTargetPreference { private boolean mIsConnected = false; /** @@ -36,7 +41,7 @@ public class AudioStreamPreference extends TwoTargetPreference { * * @param isConnected Is this streams connected */ - public void setIsConnected( + void setIsConnected( boolean isConnected, @Nullable OnPreferenceClickListener onPreferenceClickListener) { if (mIsConnected == isConnected && getOnPreferenceClickListener() == onPreferenceClickListener) { @@ -50,7 +55,7 @@ public class AudioStreamPreference extends TwoTargetPreference { notifyChanged(); } - public AudioStreamPreference(Context context, @Nullable AttributeSet attrs) { + AudioStreamPreference(Context context, @Nullable AttributeSet attrs) { super(context, attrs); setIcon(R.drawable.ic_bt_audio_sharing); } @@ -64,4 +69,34 @@ public class AudioStreamPreference extends TwoTargetPreference { protected int getSecondTargetResId() { return R.layout.preference_widget_lock; } + + static AudioStreamPreference fromMetadata( + Context context, BluetoothLeBroadcastMetadata source) { + AudioStreamPreference preference = new AudioStreamPreference(context, /* attrs= */ null); + preference.setTitle(getBroadcastName(source)); + return preference; + } + + static AudioStreamPreference fromReceiveState( + Context context, BluetoothLeBroadcastReceiveState state) { + AudioStreamPreference preference = new AudioStreamPreference(context, /* attrs= */ null); + preference.setTitle(getBroadcastName(state)); + return preference; + } + + private static String getBroadcastName(BluetoothLeBroadcastMetadata source) { + return source.getSubgroups().stream() + .map(s -> s.getContentMetadata().getProgramInfo()) + .filter(i -> !Strings.isNullOrEmpty(i)) + .findFirst() + .orElse("Broadcast Id: " + source.getBroadcastId()); + } + + private static String getBroadcastName(BluetoothLeBroadcastReceiveState state) { + return state.getSubgroupMetadata().stream() + .map(BluetoothLeAudioContentMetadata::getProgramInfo) + .filter(i -> !Strings.isNullOrEmpty(i)) + .findFirst() + .orElse("Broadcast Id: " + state.getBroadcastId()); + } } diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsBroadcastAssistantCallback.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsBroadcastAssistantCallback.java index 788b25399e6..84e753c5526 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsBroadcastAssistantCallback.java +++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsBroadcastAssistantCallback.java @@ -32,7 +32,7 @@ public class AudioStreamsBroadcastAssistantCallback private static final String TAG = "AudioStreamsBroadcastAssistantCallback"; private static final boolean DEBUG = BluetoothUtils.D; - private AudioStreamsProgressCategoryController mCategoryController; + private final AudioStreamsProgressCategoryController mCategoryController; public AudioStreamsBroadcastAssistantCallback( AudioStreamsProgressCategoryController audioStreamsProgressCategoryController) { @@ -52,6 +52,7 @@ public class AudioStreamsBroadcastAssistantCallback + " state: " + state); } + mCategoryController.handleSourceConnected(state); } @Override @@ -94,7 +95,20 @@ public class AudioStreamsBroadcastAssistantCallback @Override public void onSourceAddFailed( - BluetoothDevice sink, BluetoothLeBroadcastMetadata source, int reason) {} + BluetoothDevice sink, BluetoothLeBroadcastMetadata source, int reason) { + if (DEBUG) { + Log.d( + TAG, + "onSourceAddFailed() sink : " + + sink.getAddress() + + " source: " + + source + + " reason: " + + reason); + } + mCategoryController.showToast( + String.format(Locale.US, "Failed to join broadcast, reason %d", reason)); + } @Override public void onSourceAdded(BluetoothDevice sink, int sourceId, int reason) { @@ -119,7 +133,7 @@ public class AudioStreamsBroadcastAssistantCallback if (DEBUG) { Log.d(TAG, "onSourceFound() broadcastId : " + source.getBroadcastId()); } - mCategoryController.addSourceFound(source); + mCategoryController.handleSourceFound(source); } @Override @@ -127,7 +141,7 @@ public class AudioStreamsBroadcastAssistantCallback if (DEBUG) { Log.d(TAG, "onSourceLost() broadcastId : " + broadcastId); } - mCategoryController.removeSourceLost(broadcastId); + mCategoryController.handleSourceLost(broadcastId); } @Override @@ -137,8 +151,23 @@ public class AudioStreamsBroadcastAssistantCallback public void onSourceModifyFailed(BluetoothDevice sink, int sourceId, int reason) {} @Override - public void onSourceRemoveFailed(BluetoothDevice sink, int sourceId, int reason) {} + 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 - public void onSourceRemoved(BluetoothDevice sink, int sourceId, int reason) {} + public void onSourceRemoved(BluetoothDevice sink, int sourceId, int reason) { + 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/AudioStreamsHelper.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsHelper.java new file mode 100644 index 00000000000..5acbc1f7b93 --- /dev/null +++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsHelper.java @@ -0,0 +1,156 @@ +/* + * 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 static java.util.Collections.emptyList; + +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothLeBroadcastMetadata; +import android.bluetooth.BluetoothLeBroadcastReceiveState; +import android.util.Log; + +import com.android.settings.connecteddevice.audiosharing.AudioSharingUtils; +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.bluetooth.LocalBluetoothProfileManager; +import com.android.settingslib.utils.ThreadUtils; + +import java.util.List; +import java.util.stream.Stream; + +import javax.annotation.Nullable; + +/** + * A helper class that adds, removes and retrieves LE broadcast sources for all active sink devices. + */ +class AudioStreamsHelper { + + private static final String TAG = "AudioStreamsHelper"; + private static final boolean DEBUG = BluetoothUtils.D; + + private final @Nullable LocalBluetoothManager mBluetoothManager; + private final @Nullable LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant; + + AudioStreamsHelper(@Nullable LocalBluetoothManager bluetoothManager) { + mBluetoothManager = bluetoothManager; + mLeBroadcastAssistant = getLeBroadcastAssistant(mBluetoothManager); + } + + /** + * Adds the specified LE broadcast source to all active sinks. + * + * @param source The LE broadcast metadata representing the audio source. + */ + void addSource(BluetoothLeBroadcastMetadata source) { + if (mLeBroadcastAssistant == null) { + Log.w(TAG, "addSource(): LeBroadcastAssistant is null!"); + return; + } + var unused = + ThreadUtils.postOnBackgroundThread( + () -> { + for (var sink : getActiveSinksOnAssistant(mBluetoothManager)) { + if (DEBUG) { + Log.d( + TAG, + "addSource(): join broadcast broadcastId" + + " : " + + source.getBroadcastId() + + " sink : " + + sink.getAddress()); + } + mLeBroadcastAssistant.addSource(sink, source, false); + } + }); + } + + /** Removes all sources from LE broadcasts associated for all active sinks. */ + void removeSource() { + if (mLeBroadcastAssistant == null) { + Log.w(TAG, "removeSource(): LeBroadcastAssistant is null!"); + return; + } + var unused = + ThreadUtils.postOnBackgroundThread( + () -> { + for (var sink : getActiveSinksOnAssistant(mBluetoothManager)) { + if (DEBUG) { + Log.d( + TAG, + "removeSource(): remove all sources from sink : " + + sink.getAddress()); + } + var sources = mLeBroadcastAssistant.getAllSources(sink); + if (!sources.isEmpty()) { + mLeBroadcastAssistant.removeSource( + sink, sources.get(0).getSourceId()); + } + } + }); + } + + /** Retrieves a list of all LE broadcast receive states from active sinks. */ + List getAllSources() { + if (mLeBroadcastAssistant == null) { + Log.w(TAG, "getAllSources(): LeBroadcastAssistant is null!"); + return emptyList(); + } + return getActiveSinksOnAssistant(mBluetoothManager).stream() + .flatMap(sink -> mLeBroadcastAssistant.getAllSources(sink).stream()) + .toList(); + } + + @Nullable + LocalBluetoothLeBroadcastAssistant getLeBroadcastAssistant() { + return mLeBroadcastAssistant; + } + + private static List getActiveSinksOnAssistant( + @Nullable LocalBluetoothManager manager) { + if (manager == null) { + Log.w(TAG, "getActiveSinksOnAssistant(): LocalBluetoothManager is null!"); + return emptyList(); + } + return AudioSharingUtils.getActiveSinkOnAssistant(manager) + .map( + cachedBluetoothDevice -> + Stream.concat( + Stream.of(cachedBluetoothDevice.getDevice()), + cachedBluetoothDevice.getMemberDevice().stream() + .map(CachedBluetoothDevice::getDevice)) + .toList()) + .orElse(emptyList()); + } + + private static @Nullable LocalBluetoothLeBroadcastAssistant getLeBroadcastAssistant( + @Nullable LocalBluetoothManager manager) { + if (manager == null) { + Log.w(TAG, "getLeBroadcastAssistant(): LocalBluetoothManager is null!"); + return null; + } + + LocalBluetoothProfileManager profileManager = manager.getProfileManager(); + if (profileManager == null) { + Log.w(TAG, "getLeBroadcastAssistant(): LocalBluetoothProfileManager is null!"); + return null; + } + + return profileManager.getLeAudioBroadcastAssistantProfile(); + } +} diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java index fef1e7bf321..6cf69c5e2ef 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java +++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java @@ -18,8 +18,6 @@ package com.android.settings.connecteddevice.audiosharing.audiostreams; import static java.util.Collections.emptyList; -import android.bluetooth.BluetoothDevice; -import android.bluetooth.BluetoothLeAudioContentMetadata; import android.bluetooth.BluetoothLeBroadcastMetadata; import android.bluetooth.BluetoothLeBroadcastReceiveState; import android.content.Context; @@ -35,19 +33,12 @@ import com.android.settings.bluetooth.Utils; import com.android.settings.connecteddevice.audiosharing.AudioSharingUtils; import com.android.settings.core.BasePreferenceController; 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.bluetooth.LocalBluetoothProfileManager; import com.android.settingslib.utils.ThreadUtils; -import com.google.common.base.Strings; - -import java.util.List; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executor; import java.util.concurrent.Executors; -import java.util.stream.Stream; import javax.annotation.Nullable; @@ -58,17 +49,17 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro private final Executor mExecutor; private final AudioStreamsBroadcastAssistantCallback mBroadcastAssistantCallback; - private final LocalBluetoothManager mBluetoothManager; + private final AudioStreamsHelper mAudioStreamsHelper; private final @Nullable LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant; private final ConcurrentHashMap mBroadcastIdToPreferenceMap = new ConcurrentHashMap<>(); - private @Nullable AudioStreamsProgressCategoryPreference mCategoryPreference; + private AudioStreamsProgressCategoryPreference mCategoryPreference; public AudioStreamsProgressCategoryController(Context context, String preferenceKey) { super(context, preferenceKey); mExecutor = Executors.newSingleThreadExecutor(); - mBluetoothManager = Utils.getLocalBtManager(mContext); - mLeBroadcastAssistant = getLeBroadcastAssistant(mBluetoothManager); + mAudioStreamsHelper = new AudioStreamsHelper(Utils.getLocalBtManager(mContext)); + mLeBroadcastAssistant = mAudioStreamsHelper.getLeBroadcastAssistant(); mBroadcastAssistantCallback = new AudioStreamsBroadcastAssistantCallback(this); } @@ -104,14 +95,10 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro // Display currently connected streams var unused = ThreadUtils.postOnBackgroundThread( - () -> { - for (var sink : - getActiveSinksOnAssistant(mBluetoothManager)) { - mLeBroadcastAssistant - .getAllSources(sink) - .forEach(this::addSourceConnected); - } - }); + () -> + mAudioStreamsHelper + .getAllSources() + .forEach(this::handleSourceConnected)); }); } @@ -140,31 +127,36 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro }); } - void addSourceFound(BluetoothLeBroadcastMetadata source) { - Preference.OnPreferenceClickListener onClickListener = + void handleSourceFound(BluetoothLeBroadcastMetadata source) { + Preference.OnPreferenceClickListener addSourceOrShowDialog = preference -> { if (DEBUG) { Log.d(TAG, "preferenceClicked(): attempt to join broadcast"); } - - // TODO(chelseahao): add source to sink + if (source.isEncrypted()) { + ThreadUtils.postOnMainThread( + () -> launchPasswordDialog(source, preference)); + } else { + mAudioStreamsHelper.addSource(source); + } return true; }; mBroadcastIdToPreferenceMap.computeIfAbsent( source.getBroadcastId(), k -> { - var p = createPreference(source, onClickListener); + var preference = AudioStreamPreference.fromMetadata(mContext, source); ThreadUtils.postOnMainThread( () -> { + preference.setIsConnected(false, addSourceOrShowDialog); if (mCategoryPreference != null) { - mCategoryPreference.addPreference(p); + mCategoryPreference.addPreference(preference); } }); - return p; + return preference; }); } - void removeSourceLost(int broadcastId) { + void handleSourceLost(int broadcastId) { var toRemove = mBroadcastIdToPreferenceMap.remove(broadcastId); if (toRemove != null) { ThreadUtils.postOnMainThread( @@ -174,92 +166,43 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro } }); } - // TODO(chelseahao): remove source from sink + mAudioStreamsHelper.removeSource(); } - private void addSourceConnected(BluetoothLeBroadcastReceiveState state) { + void handleSourceConnected(BluetoothLeBroadcastReceiveState state) { + // TODO(chelseahao): only continue when the state indicates a successful connection mBroadcastIdToPreferenceMap.compute( state.getBroadcastId(), (k, v) -> { - if (v == null) { - // Create a new preference as the source has not been added. - var p = createPreference(state); - ThreadUtils.postOnMainThread( - () -> { - if (mCategoryPreference != null) { - mCategoryPreference.addPreference(p); - } - }); - return p; - } else { - // This source has been added either by scanning, or it's currently - // connected to another active sink. Update its connection status to true - // if needed. - ThreadUtils.postOnMainThread(() -> v.setIsConnected(true, null)); - return v; - } + // True if this source has been added either by scanning, or it's currently + // connected to another active sink. + boolean existed = v != null; + AudioStreamPreference preference = + existed ? v : AudioStreamPreference.fromReceiveState(mContext, state); + + ThreadUtils.postOnMainThread( + () -> { + preference.setIsConnected( + true, p -> launchDetailFragment((AudioStreamPreference) p)); + if (mCategoryPreference != null && !existed) { + mCategoryPreference.addPreference(preference); + } + }); + + return preference; }); } - private AudioStreamPreference createPreference( - BluetoothLeBroadcastMetadata source, - Preference.OnPreferenceClickListener onPreferenceClickListener) { - AudioStreamPreference preference = new AudioStreamPreference(mContext, /* attrs= */ null); - preference.setTitle( - source.getSubgroups().stream() - .map(s -> s.getContentMetadata().getProgramInfo()) - .filter(i -> !Strings.isNullOrEmpty(i)) - .findFirst() - .orElse("Broadcast Id: " + source.getBroadcastId())); - preference.setIsConnected(false, onPreferenceClickListener); - return preference; - } - - private AudioStreamPreference createPreference(BluetoothLeBroadcastReceiveState state) { - AudioStreamPreference preference = new AudioStreamPreference(mContext, /* attrs= */ null); - preference.setTitle( - state.getSubgroupMetadata().stream() - .map(BluetoothLeAudioContentMetadata::getProgramInfo) - .filter(i -> !Strings.isNullOrEmpty(i)) - .findFirst() - .orElse("Broadcast Id: " + state.getBroadcastId())); - preference.setIsConnected(true, null); - return preference; - } - - private static List getActiveSinksOnAssistant(LocalBluetoothManager manager) { - if (manager == null) { - Log.w(TAG, "getActiveSinksOnAssistant(): LocalBluetoothManager is null!"); - return emptyList(); - } - return AudioSharingUtils.getActiveSinkOnAssistant(manager) - .map( - cachedBluetoothDevice -> - Stream.concat( - Stream.of(cachedBluetoothDevice.getDevice()), - cachedBluetoothDevice.getMemberDevice().stream() - .map(CachedBluetoothDevice::getDevice)) - .toList()) - .orElse(emptyList()); - } - - private static @Nullable LocalBluetoothLeBroadcastAssistant getLeBroadcastAssistant( - LocalBluetoothManager manager) { - if (manager == null) { - Log.w(TAG, "getLeBroadcastAssistant(): LocalBluetoothManager is null!"); - return null; - } - - LocalBluetoothProfileManager profileManager = manager.getProfileManager(); - if (profileManager == null) { - Log.w(TAG, "getLeBroadcastAssistant(): LocalBluetoothProfileManager is null!"); - return null; - } - - return profileManager.getLeAudioBroadcastAssistantProfile(); - } - void showToast(String msg) { AudioSharingUtils.toastMessage(mContext, msg); } + + private boolean launchDetailFragment(AudioStreamPreference preference) { + // TODO(chelseahao): impl + return true; + } + + private void launchPasswordDialog(BluetoothLeBroadcastMetadata source, Preference preference) { + // TODO(chelseahao): impl + } }