[AudioStream] Hysteresis mode support

Flag: com.android.settingslib.flags.audio_sharing_hysteresis_mode_fix
Test: atest com.android.settings.connecteddevice.audiosharing.audiostreams
Test: manual test with broadcast hysteresis mode
Bug: 355222285
Bug: 355221818
Change-Id: If3a1fbdc391eeda6979868829bc00c435a43c329
This commit is contained in:
Rongxuan Liu
2024-08-29 20:23:48 +00:00
parent c3cfb42524
commit 2c3f54c5e3
16 changed files with 719 additions and 28 deletions

View File

@@ -16,6 +16,8 @@
package com.android.settings.connecteddevice.audiosharing.audiostreams;
import static com.android.settingslib.flags.Flags.audioSharingHysteresisModeFix;
import android.app.settings.SettingsEnums;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothLeBroadcastAssistant;
@@ -41,6 +43,7 @@ import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
import com.android.settingslib.utils.ThreadUtils;
import com.android.settingslib.widget.ActionButtonsPreference;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
@@ -73,12 +76,18 @@ public class AudioStreamButtonController extends BasePreferenceController
int sourceId,
BluetoothLeBroadcastReceiveState state) {
super.onReceiveStateChanged(sink, sourceId, state);
if (AudioStreamsHelper.isConnected(state)) {
boolean shouldUpdateButton =
audioSharingHysteresisModeFix()
? AudioStreamsHelper.hasSourcePresent(state)
: AudioStreamsHelper.isConnected(state);
if (shouldUpdateButton) {
updateButton();
mMetricsFeatureProvider.action(
mContext,
SettingsEnums.ACTION_AUDIO_STREAM_JOIN_SUCCEED,
SOURCE_ORIGIN_REPOSITORY);
if (AudioStreamsHelper.isConnected(state)) {
mMetricsFeatureProvider.action(
mContext,
SettingsEnums.ACTION_AUDIO_STREAM_JOIN_SUCCEED,
SOURCE_ORIGIN_REPOSITORY);
}
}
}
@@ -146,8 +155,13 @@ public class AudioStreamButtonController extends BasePreferenceController
Log.w(TAG, "updateButton(): preference is null!");
return;
}
List<BluetoothLeBroadcastReceiveState> sources =
audioSharingHysteresisModeFix()
? mAudioStreamsHelper.getAllPresentSources()
: mAudioStreamsHelper.getAllConnectedSources();
boolean isConnected =
mAudioStreamsHelper.getAllConnectedSources().stream()
sources.stream()
.map(BluetoothLeBroadcastReceiveState::getBroadcastId)
.anyMatch(connectedBroadcastId -> connectedBroadcastId == mBroadcastId);

View File

@@ -16,6 +16,10 @@
package com.android.settings.connecteddevice.audiosharing.audiostreams;
import static com.android.settingslib.flags.Flags.audioSharingHysteresisModeFix;
import static java.util.stream.Collectors.toList;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothLeBroadcastAssistant;
import android.bluetooth.BluetoothLeBroadcastReceiveState;
@@ -48,6 +52,8 @@ public class AudioStreamHeaderController extends BasePreferenceController
static final int AUDIO_STREAM_HEADER_LISTENING_NOW_SUMMARY =
R.string.audio_streams_listening_now;
static final int AUDIO_STREAM_HEADER_PRESENT_NOW_SUMMARY = R.string.audio_streams_present_now;
@VisibleForTesting static final String AUDIO_STREAM_HEADER_NOT_LISTENING_SUMMARY = "";
private static final String TAG = "AudioStreamHeaderController";
private static final String KEY = "audio_stream_header";
@@ -80,6 +86,10 @@ public class AudioStreamHeaderController extends BasePreferenceController
updateSummary();
mAudioStreamsHelper.startMediaService(
mContext, mBroadcastId, mBroadcastName);
} else if (audioSharingHysteresisModeFix()
&& AudioStreamsHelper.hasSourcePresent(state)) {
// if source present but not connected, only update the summary
updateSummary();
}
}
};
@@ -140,8 +150,27 @@ public class AudioStreamHeaderController extends BasePreferenceController
var unused =
ThreadUtils.postOnBackgroundThread(
() -> {
var connectedSourceList =
mAudioStreamsHelper.getAllPresentSources().stream()
.filter(
state ->
(state.getBroadcastId()
== mBroadcastId))
.collect(toList());
var latestSummary =
mAudioStreamsHelper.getAllConnectedSources().stream()
audioSharingHysteresisModeFix()
? connectedSourceList.isEmpty()
? AUDIO_STREAM_HEADER_NOT_LISTENING_SUMMARY
: (connectedSourceList.stream()
.anyMatch(
AudioStreamsHelper
::isConnected)
? mContext.getString(
AUDIO_STREAM_HEADER_LISTENING_NOW_SUMMARY)
: mContext.getString(
AUDIO_STREAM_HEADER_PRESENT_NOW_SUMMARY))
: mAudioStreamsHelper.getAllConnectedSources().stream()
.map(
BluetoothLeBroadcastReceiveState
::getBroadcastId)
@@ -149,9 +178,10 @@ public class AudioStreamHeaderController extends BasePreferenceController
connectedBroadcastId ->
connectedBroadcastId
== mBroadcastId)
? mContext.getString(
AUDIO_STREAM_HEADER_LISTENING_NOW_SUMMARY)
: AUDIO_STREAM_HEADER_NOT_LISTENING_SUMMARY;
? mContext.getString(
AUDIO_STREAM_HEADER_LISTENING_NOW_SUMMARY)
: AUDIO_STREAM_HEADER_NOT_LISTENING_SUMMARY;
ThreadUtils.postOnMainThread(
() -> {
if (mHeaderController != null) {

View File

@@ -18,6 +18,8 @@ package com.android.settings.connecteddevice.audiosharing.audiostreams;
import static android.text.Spanned.SPAN_EXCLUSIVE_INCLUSIVE;
import static com.android.settingslib.flags.Flags.audioSharingHysteresisModeFix;
import android.os.Handler;
import android.os.Looper;
import android.text.SpannableString;
@@ -94,8 +96,12 @@ class AudioStreamStateHandler {
}
preference.setIsConnected(
newState
== AudioStreamsProgressCategoryController.AudioStreamState
.SOURCE_ADDED);
== AudioStreamsProgressCategoryController
.AudioStreamState.SOURCE_ADDED
|| (audioSharingHysteresisModeFix()
&& newState
== AudioStreamsProgressCategoryController
.AudioStreamState.SOURCE_PRESENT));
preference.setOnPreferenceClickListener(getOnClickListener(controller));
});
}

View File

@@ -19,6 +19,7 @@ package com.android.settings.connecteddevice.audiosharing.audiostreams;
import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamMediaService.BROADCAST_ID;
import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamMediaService.BROADCAST_TITLE;
import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamMediaService.DEVICES;
import static com.android.settingslib.flags.Flags.audioSharingHysteresisModeFix;
import static java.util.Collections.emptyList;
@@ -63,6 +64,12 @@ public class AudioStreamsHelper {
private final @Nullable LocalBluetoothManager mBluetoothManager;
private final @Nullable LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant;
// Referring to Broadcast Audio Scan Service 1.0
// Table 3.9: Broadcast Receive State characteristic format
// 0x00000000: 0b0 = Not synchronized to BIS_index[x]
// 0xFFFFFFFF: Failed to sync to BIG
private static final long BIS_SYNC_NOT_SYNC_TO_BIS = 0x00000000L;
private static final long BIS_SYNC_FAILED_SYNC_TO_BIG = 0xFFFFFFFFL;
AudioStreamsHelper(@Nullable LocalBluetoothManager bluetoothManager) {
mBluetoothManager = bluetoothManager;
@@ -144,6 +151,19 @@ public class AudioStreamsHelper {
.toList();
}
/** Retrieves a list of all LE broadcast receive states from sinks with source present. */
@VisibleForTesting
public List<BluetoothLeBroadcastReceiveState> getAllPresentSources() {
if (mLeBroadcastAssistant == null) {
Log.w(TAG, "getAllPresentSources(): LeBroadcastAssistant is null!");
return emptyList();
}
return getConnectedBluetoothDevices(mBluetoothManager, /* inSharingOnly= */ true).stream()
.flatMap(sink -> mLeBroadcastAssistant.getAllSources(sink).stream())
.filter(AudioStreamsHelper::hasSourcePresent)
.toList();
}
/** Retrieves LocalBluetoothLeBroadcastAssistant. */
@VisibleForTesting
@Nullable
@@ -153,7 +173,18 @@ public class AudioStreamsHelper {
/** Checks the connectivity status based on the provided broadcast receive state. */
public static boolean isConnected(BluetoothLeBroadcastReceiveState state) {
return state.getBisSyncState().stream().anyMatch(bitmap -> bitmap != 0);
return state.getBisSyncState().stream()
.anyMatch(
bitmap ->
(bitmap != BIS_SYNC_NOT_SYNC_TO_BIS
&& bitmap != BIS_SYNC_FAILED_SYNC_TO_BIG));
}
/** Checks the connectivity status based on the provided broadcast receive state. */
public static boolean hasSourcePresent(BluetoothLeBroadcastReceiveState state) {
// Referring to Broadcast Audio Scan Service 1.0
// All zero address means no source on the sink device
return !state.getSourceDevice().getAddress().equals("00:00:00:00:00:00");
}
static boolean isBadCode(BluetoothLeBroadcastReceiveState state) {
@@ -242,7 +273,8 @@ public class AudioStreamsHelper {
List<BluetoothLeBroadcastReceiveState> sourceList =
assistant.getAllSources(cachedDevice.getDevice());
if (!sourceList.isEmpty()
&& sourceList.stream().anyMatch(AudioStreamsHelper::isConnected)) {
&& (audioSharingHysteresisModeFix()
|| sourceList.stream().anyMatch(AudioStreamsHelper::isConnected))) {
Log.d(
TAG,
"Lead device has connected broadcast source, device = "
@@ -253,7 +285,9 @@ public class AudioStreamsHelper {
for (CachedBluetoothDevice device : cachedDevice.getMemberDevice()) {
List<BluetoothLeBroadcastReceiveState> list =
assistant.getAllSources(device.getDevice());
if (!list.isEmpty() && list.stream().anyMatch(AudioStreamsHelper::isConnected)) {
if (!list.isEmpty()
&& (audioSharingHysteresisModeFix()
|| list.stream().anyMatch(AudioStreamsHelper::isConnected))) {
Log.d(
TAG,
"Member device has connected broadcast source, device = "

View File

@@ -16,6 +16,8 @@
package com.android.settings.connecteddevice.audiosharing.audiostreams;
import static com.android.settingslib.flags.Flags.audioSharingHysteresisModeFix;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothLeBroadcastMetadata;
import android.bluetooth.BluetoothLeBroadcastReceiveState;
@@ -39,6 +41,9 @@ public class AudioStreamsProgressCategoryCallback extends AudioStreamsBroadcastA
mCategoryController.handleSourceConnected(state);
} else if (AudioStreamsHelper.isBadCode(state)) {
mCategoryController.handleSourceConnectBadCode(state);
} else if (audioSharingHysteresisModeFix() && AudioStreamsHelper.hasSourcePresent(state)) {
// Keep this check as the last, source might also present in above states
mCategoryController.handleSourcePresent(state);
}
}

View File

@@ -16,6 +16,8 @@
package com.android.settings.connecteddevice.audiosharing.audiostreams;
import static com.android.settingslib.flags.Flags.audioSharingHysteresisModeFix;
import static java.util.Collections.emptyList;
import android.app.AlertDialog;
@@ -48,6 +50,7 @@ import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.settingslib.utils.ThreadUtils;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
@@ -95,9 +98,14 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
private final Comparator<AudioStreamPreference> mComparator =
Comparator.<AudioStreamPreference, Boolean>comparing(
p ->
p.getAudioStreamState()
== AudioStreamsProgressCategoryController
.AudioStreamState.SOURCE_ADDED)
(p.getAudioStreamState()
== AudioStreamsProgressCategoryController
.AudioStreamState.SOURCE_ADDED
|| (audioSharingHysteresisModeFix()
&& p.getAudioStreamState()
== AudioStreamsProgressCategoryController
.AudioStreamState
.SOURCE_PRESENT)))
.thenComparingInt(AudioStreamPreference::getAudioStreamRssi)
.reversed();
@@ -113,6 +121,8 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
ADD_SOURCE_BAD_CODE,
// When addSource result in other bad state.
ADD_SOURCE_FAILED,
// Source is present on sink.
SOURCE_PRESENT,
// Source is added to active sink.
SOURCE_ADDED,
}
@@ -243,10 +253,13 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
existingPreference, AudioStreamState.ADD_SOURCE_WAIT_FOR_RESPONSE);
} else {
// A preference with source founded existed either because it's already
// connected (SOURCE_ADDED). Any other reason is unexpected. We update the
// preference with this source and won't change it's state.
// connected (SOURCE_ADDED) or present (SOURCE_PRESENT). Any other reason
// is unexpected. We update the preference with this source and won't
// change it's state.
existingPreference.setAudioStreamMetadata(source);
if (fromState != AudioStreamState.SOURCE_ADDED) {
if (fromState != AudioStreamState.SOURCE_ADDED
&& (!audioSharingHysteresisModeFix()
|| fromState != AudioStreamState.SOURCE_PRESENT)) {
Log.w(
TAG,
"handleSourceFound(): unexpected state : "
@@ -346,10 +359,14 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
for (var entry : mBroadcastIdToPreferenceMap.entrySet()) {
var preference = entry.getValue();
// Look for preference has SOURCE_ADDED state, re-check if they are still connected. If
// Look for preference has SOURCE_ADDED or SOURCE_PRESENT state, re-check if they are
// still connected. If
// not, means the source is removed from the sink, we move back the preference to SYNCED
// state.
if (preference.getAudioStreamState() == AudioStreamState.SOURCE_ADDED
if ((preference.getAudioStreamState() == AudioStreamState.SOURCE_ADDED
|| (audioSharingHysteresisModeFix()
&& preference.getAudioStreamState()
== AudioStreamState.SOURCE_PRESENT))
&& mAudioStreamsHelper.getAllConnectedSources().stream()
.noneMatch(
connected ->
@@ -383,6 +400,7 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
if (!AudioStreamsHelper.isConnected(receiveState)) {
return;
}
var broadcastIdConnected = receiveState.getBroadcastId();
if (mSourceFromQrCode != null && mSourceFromQrCode.getBroadcastId() == UNSET_BROADCAST_ID) {
// mSourceFromQrCode could have no broadcast Id, we fill in the broadcast Id from the
@@ -455,6 +473,58 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
});
}
// Find preference by receiveState and decide next state.
// Expect one preference existed, move to SOURCE_PRESENT
void handleSourcePresent(BluetoothLeBroadcastReceiveState receiveState) {
if (DEBUG) {
Log.d(TAG, "handleSourcePresent()");
}
if (!AudioStreamsHelper.hasSourcePresent(receiveState)) {
return;
}
var broadcastIdConnected = receiveState.getBroadcastId();
if (mSourceFromQrCode != null && mSourceFromQrCode.getBroadcastId() == UNSET_BROADCAST_ID) {
// mSourceFromQrCode could have no broadcast Id, we fill in the broadcast Id from the
// connected source receiveState.
if (DEBUG) {
Log.d(
TAG,
"handleSourcePresent() : processing mSourceFromQrCode with broadcastId"
+ " unset");
}
boolean updated =
maybeUpdateId(
AudioStreamsHelper.getBroadcastName(receiveState),
receiveState.getBroadcastId());
if (updated && mBroadcastIdToPreferenceMap.containsKey(UNSET_BROADCAST_ID)) {
var preference = mBroadcastIdToPreferenceMap.remove(UNSET_BROADCAST_ID);
mBroadcastIdToPreferenceMap.put(receiveState.getBroadcastId(), preference);
}
}
mBroadcastIdToPreferenceMap.compute(
broadcastIdConnected,
(k, existingPreference) -> {
if (existingPreference == null) {
// No existing preference for this source even if it's already connected,
// add one and set initial state to SOURCE_PRESENT. This could happen
// because
// we retrieves the connected source during onStart() from
// AudioStreamsHelper#getAllPresentSources() even before the source is
// founded by scanning.
return addNewPreference(receiveState, AudioStreamState.SOURCE_PRESENT);
}
if (existingPreference.getAudioStreamState() == AudioStreamState.WAIT_FOR_SYNC
&& existingPreference.getAudioStreamBroadcastId() == UNSET_BROADCAST_ID
&& mSourceFromQrCode != null) {
existingPreference.setAudioStreamMetadata(mSourceFromQrCode);
}
moveToState(existingPreference, AudioStreamState.SOURCE_PRESENT);
return existingPreference;
});
}
// Find preference by metadata and decide next state.
// Expect one preference existed, move to ADD_SOURCE_WAIT_FOR_RESPONSE
void handleSourceAddRequest(
@@ -530,9 +600,23 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
// Handle QR code scan, display currently connected streams then start scanning
// sequentially
handleSourceFromQrCodeIfExists();
mAudioStreamsHelper
.getAllConnectedSources()
.forEach(this::handleSourceConnected);
if (audioSharingHysteresisModeFix()) {
// With hysteresis mode, we prioritize showing connected sources first.
// If no connected sources are found, we then show present sources.
List<BluetoothLeBroadcastReceiveState> sources =
mAudioStreamsHelper.getAllConnectedSources();
if (!sources.isEmpty()) {
sources.forEach(this::handleSourceConnected);
} else {
mAudioStreamsHelper
.getAllPresentSources()
.forEach(this::handleSourcePresent);
}
} else {
mAudioStreamsHelper
.getAllConnectedSources()
.forEach(this::handleSourceConnected);
}
mLeBroadcastAssistant.startSearchingForSources(emptyList());
mMediaControlHelper.start();
});
@@ -581,6 +665,7 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
AddSourceWaitForResponseState.getInstance();
case ADD_SOURCE_BAD_CODE -> AddSourceBadCodeState.getInstance();
case ADD_SOURCE_FAILED -> AddSourceFailedState.getInstance();
case SOURCE_PRESENT -> SourcePresentState.getInstance();
case SOURCE_ADDED -> SourceAddedState.getInstance();
default -> throw new IllegalArgumentException("Unsupported state: " + state);
};

View File

@@ -0,0 +1,87 @@
/*
* 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.app.settings.SettingsEnums;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;
import com.android.settings.R;
import com.android.settings.core.SubSettingLauncher;
import com.android.settings.dashboard.DashboardFragment;
class SourcePresentState extends AudioStreamStateHandler {
@VisibleForTesting
static final int AUDIO_STREAM_SOURCE_PRESENT_STATE_SUMMARY = R.string.audio_streams_present_now;
@Nullable private static SourcePresentState sInstance = null;
SourcePresentState() {}
static SourcePresentState getInstance() {
if (sInstance == null) {
sInstance = new SourcePresentState();
}
return sInstance;
}
@Override
void performAction(
AudioStreamPreference preference,
AudioStreamsProgressCategoryController controller,
AudioStreamsHelper helper) {
// nothing to do
}
@Override
int getSummary() {
return AUDIO_STREAM_SOURCE_PRESENT_STATE_SUMMARY;
}
@Override
Preference.OnPreferenceClickListener getOnClickListener(
AudioStreamsProgressCategoryController controller) {
return preference -> {
var p = (AudioStreamPreference) preference;
Bundle broadcast = new Bundle();
broadcast.putString(
AudioStreamDetailsFragment.BROADCAST_NAME_ARG, (String) p.getTitle());
broadcast.putInt(
AudioStreamDetailsFragment.BROADCAST_ID_ARG, p.getAudioStreamBroadcastId());
new SubSettingLauncher(p.getContext())
.setTitleRes(R.string.audio_streams_detail_page_title)
.setDestination(AudioStreamDetailsFragment.class.getName())
.setSourceMetricsCategory(
!(controller.getFragment() instanceof DashboardFragment)
? SettingsEnums.PAGE_UNKNOWN
: ((DashboardFragment) controller.getFragment())
.getMetricsCategory())
.setArguments(broadcast)
.launch();
return true;
};
}
@Override
AudioStreamsProgressCategoryController.AudioStreamState getStateEnum() {
return AudioStreamsProgressCategoryController.AudioStreamState.SOURCE_PRESENT;
}
}