Merge "[Audiosharing] Set visibility by active device." into main

This commit is contained in:
Chelsea Hao
2023-12-14 03:27:35 +00:00
committed by Android (Google) Code Review
7 changed files with 289 additions and 55 deletions

View File

@@ -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>

View File

@@ -41,6 +41,8 @@ import java.util.Map;
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 @@ public class AudioSharingUtils {
* @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();

View File

@@ -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;
}
}

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -80,8 +80,8 @@ class AudioStreamsHelper {
});
}
/** 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 @@ class AudioStreamsHelper {
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 @@ class AudioStreamsHelper {
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) {

View File

@@ -22,9 +22,9 @@ import android.app.AlertDialog;
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.bluetooth.Utils;
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 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
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 class AudioStreamsProgressCategoryController extends BasePreferenceContro
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 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
@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 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
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 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
}
});
}
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 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
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 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
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 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
(dialog, which) -> {
var code =
((EditText)
layout.requireViewById(
R.id.broadcast_edit_text))
layout.requireViewById(
R.id.broadcast_edit_text))
.getText()
.toString();
mAudioStreamsHelper.addSource(