[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

@@ -13553,6 +13553,8 @@
<string name="audio_streams_dialog_cannot_play">Can\u0027t play this audio stream on <xliff:g example="LE headset" id="device_name">%1$s</xliff:g>.</string> <string name="audio_streams_dialog_cannot_play">Can\u0027t play this audio stream on <xliff:g example="LE headset" id="device_name">%1$s</xliff:g>.</string>
<!-- The preference summary when add source succeed [CHAR LIMIT=NONE] --> <!-- The preference summary when add source succeed [CHAR LIMIT=NONE] -->
<string name="audio_streams_listening_now">Listening now</string> <string name="audio_streams_listening_now">Listening now</string>
<!-- The preference summary when source is present on sinks [CHAR LIMIT=NONE] -->
<string name="audio_streams_present_now">Paused by host</string>
<!-- Le audio streams service notification leave broadcast text [CHAR LIMIT=NONE] --> <!-- Le audio streams service notification leave broadcast text [CHAR LIMIT=NONE] -->
<string name="audio_streams_media_service_notification_leave_broadcast_text">Stop listening</string> <string name="audio_streams_media_service_notification_leave_broadcast_text">Stop listening</string>
<!-- Le audio streams no le device dialog title [CHAR LIMIT=NONE] --> <!-- Le audio streams no le device dialog title [CHAR LIMIT=NONE] -->

View File

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

View File

@@ -16,6 +16,10 @@
package com.android.settings.connecteddevice.audiosharing.audiostreams; 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.BluetoothDevice;
import android.bluetooth.BluetoothLeBroadcastAssistant; import android.bluetooth.BluetoothLeBroadcastAssistant;
import android.bluetooth.BluetoothLeBroadcastReceiveState; import android.bluetooth.BluetoothLeBroadcastReceiveState;
@@ -48,6 +52,8 @@ public class AudioStreamHeaderController extends BasePreferenceController
static final int AUDIO_STREAM_HEADER_LISTENING_NOW_SUMMARY = static final int AUDIO_STREAM_HEADER_LISTENING_NOW_SUMMARY =
R.string.audio_streams_listening_now; 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 = ""; @VisibleForTesting static final String AUDIO_STREAM_HEADER_NOT_LISTENING_SUMMARY = "";
private static final String TAG = "AudioStreamHeaderController"; private static final String TAG = "AudioStreamHeaderController";
private static final String KEY = "audio_stream_header"; private static final String KEY = "audio_stream_header";
@@ -80,6 +86,10 @@ public class AudioStreamHeaderController extends BasePreferenceController
updateSummary(); updateSummary();
mAudioStreamsHelper.startMediaService( mAudioStreamsHelper.startMediaService(
mContext, mBroadcastId, mBroadcastName); 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 = var unused =
ThreadUtils.postOnBackgroundThread( ThreadUtils.postOnBackgroundThread(
() -> { () -> {
var connectedSourceList =
mAudioStreamsHelper.getAllPresentSources().stream()
.filter(
state ->
(state.getBroadcastId()
== mBroadcastId))
.collect(toList());
var latestSummary = 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( .map(
BluetoothLeBroadcastReceiveState BluetoothLeBroadcastReceiveState
::getBroadcastId) ::getBroadcastId)
@@ -149,9 +178,10 @@ public class AudioStreamHeaderController extends BasePreferenceController
connectedBroadcastId -> connectedBroadcastId ->
connectedBroadcastId connectedBroadcastId
== mBroadcastId) == mBroadcastId)
? mContext.getString( ? mContext.getString(
AUDIO_STREAM_HEADER_LISTENING_NOW_SUMMARY) AUDIO_STREAM_HEADER_LISTENING_NOW_SUMMARY)
: AUDIO_STREAM_HEADER_NOT_LISTENING_SUMMARY; : AUDIO_STREAM_HEADER_NOT_LISTENING_SUMMARY;
ThreadUtils.postOnMainThread( ThreadUtils.postOnMainThread(
() -> { () -> {
if (mHeaderController != null) { 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 android.text.Spanned.SPAN_EXCLUSIVE_INCLUSIVE;
import static com.android.settingslib.flags.Flags.audioSharingHysteresisModeFix;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.text.SpannableString; import android.text.SpannableString;
@@ -94,8 +96,12 @@ class AudioStreamStateHandler {
} }
preference.setIsConnected( preference.setIsConnected(
newState newState
== AudioStreamsProgressCategoryController.AudioStreamState == AudioStreamsProgressCategoryController
.SOURCE_ADDED); .AudioStreamState.SOURCE_ADDED
|| (audioSharingHysteresisModeFix()
&& newState
== AudioStreamsProgressCategoryController
.AudioStreamState.SOURCE_PRESENT));
preference.setOnPreferenceClickListener(getOnClickListener(controller)); 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_ID;
import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamMediaService.BROADCAST_TITLE; 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.settings.connecteddevice.audiosharing.audiostreams.AudioStreamMediaService.DEVICES;
import static com.android.settingslib.flags.Flags.audioSharingHysteresisModeFix;
import static java.util.Collections.emptyList; import static java.util.Collections.emptyList;
@@ -63,6 +64,12 @@ public class AudioStreamsHelper {
private final @Nullable LocalBluetoothManager mBluetoothManager; private final @Nullable LocalBluetoothManager mBluetoothManager;
private final @Nullable LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant; 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) { AudioStreamsHelper(@Nullable LocalBluetoothManager bluetoothManager) {
mBluetoothManager = bluetoothManager; mBluetoothManager = bluetoothManager;
@@ -144,6 +151,19 @@ public class AudioStreamsHelper {
.toList(); .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. */ /** Retrieves LocalBluetoothLeBroadcastAssistant. */
@VisibleForTesting @VisibleForTesting
@Nullable @Nullable
@@ -153,7 +173,18 @@ public class AudioStreamsHelper {
/** Checks the connectivity status based on the provided broadcast receive state. */ /** Checks the connectivity status based on the provided broadcast receive state. */
public static boolean isConnected(BluetoothLeBroadcastReceiveState 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) { static boolean isBadCode(BluetoothLeBroadcastReceiveState state) {
@@ -242,7 +273,8 @@ public class AudioStreamsHelper {
List<BluetoothLeBroadcastReceiveState> sourceList = List<BluetoothLeBroadcastReceiveState> sourceList =
assistant.getAllSources(cachedDevice.getDevice()); assistant.getAllSources(cachedDevice.getDevice());
if (!sourceList.isEmpty() if (!sourceList.isEmpty()
&& sourceList.stream().anyMatch(AudioStreamsHelper::isConnected)) { && (audioSharingHysteresisModeFix()
|| sourceList.stream().anyMatch(AudioStreamsHelper::isConnected))) {
Log.d( Log.d(
TAG, TAG,
"Lead device has connected broadcast source, device = " "Lead device has connected broadcast source, device = "
@@ -253,7 +285,9 @@ public class AudioStreamsHelper {
for (CachedBluetoothDevice device : cachedDevice.getMemberDevice()) { for (CachedBluetoothDevice device : cachedDevice.getMemberDevice()) {
List<BluetoothLeBroadcastReceiveState> list = List<BluetoothLeBroadcastReceiveState> list =
assistant.getAllSources(device.getDevice()); assistant.getAllSources(device.getDevice());
if (!list.isEmpty() && list.stream().anyMatch(AudioStreamsHelper::isConnected)) { if (!list.isEmpty()
&& (audioSharingHysteresisModeFix()
|| list.stream().anyMatch(AudioStreamsHelper::isConnected))) {
Log.d( Log.d(
TAG, TAG,
"Member device has connected broadcast source, device = " "Member device has connected broadcast source, device = "

View File

@@ -16,6 +16,8 @@
package com.android.settings.connecteddevice.audiosharing.audiostreams; package com.android.settings.connecteddevice.audiosharing.audiostreams;
import static com.android.settingslib.flags.Flags.audioSharingHysteresisModeFix;
import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothLeBroadcastMetadata; import android.bluetooth.BluetoothLeBroadcastMetadata;
import android.bluetooth.BluetoothLeBroadcastReceiveState; import android.bluetooth.BluetoothLeBroadcastReceiveState;
@@ -39,6 +41,9 @@ public class AudioStreamsProgressCategoryCallback extends AudioStreamsBroadcastA
mCategoryController.handleSourceConnected(state); mCategoryController.handleSourceConnected(state);
} else if (AudioStreamsHelper.isBadCode(state)) { } else if (AudioStreamsHelper.isBadCode(state)) {
mCategoryController.handleSourceConnectBadCode(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; package com.android.settings.connecteddevice.audiosharing.audiostreams;
import static com.android.settingslib.flags.Flags.audioSharingHysteresisModeFix;
import static java.util.Collections.emptyList; import static java.util.Collections.emptyList;
import android.app.AlertDialog; import android.app.AlertDialog;
@@ -48,6 +50,7 @@ import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.settingslib.utils.ThreadUtils; import com.android.settingslib.utils.ThreadUtils;
import java.util.Comparator; import java.util.Comparator;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
@@ -95,9 +98,14 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
private final Comparator<AudioStreamPreference> mComparator = private final Comparator<AudioStreamPreference> mComparator =
Comparator.<AudioStreamPreference, Boolean>comparing( Comparator.<AudioStreamPreference, Boolean>comparing(
p -> p ->
p.getAudioStreamState() (p.getAudioStreamState()
== AudioStreamsProgressCategoryController == AudioStreamsProgressCategoryController
.AudioStreamState.SOURCE_ADDED) .AudioStreamState.SOURCE_ADDED
|| (audioSharingHysteresisModeFix()
&& p.getAudioStreamState()
== AudioStreamsProgressCategoryController
.AudioStreamState
.SOURCE_PRESENT)))
.thenComparingInt(AudioStreamPreference::getAudioStreamRssi) .thenComparingInt(AudioStreamPreference::getAudioStreamRssi)
.reversed(); .reversed();
@@ -113,6 +121,8 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
ADD_SOURCE_BAD_CODE, ADD_SOURCE_BAD_CODE,
// When addSource result in other bad state. // When addSource result in other bad state.
ADD_SOURCE_FAILED, ADD_SOURCE_FAILED,
// Source is present on sink.
SOURCE_PRESENT,
// Source is added to active sink. // Source is added to active sink.
SOURCE_ADDED, SOURCE_ADDED,
} }
@@ -243,10 +253,13 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
existingPreference, AudioStreamState.ADD_SOURCE_WAIT_FOR_RESPONSE); existingPreference, AudioStreamState.ADD_SOURCE_WAIT_FOR_RESPONSE);
} else { } else {
// A preference with source founded existed either because it's already // A preference with source founded existed either because it's already
// connected (SOURCE_ADDED). Any other reason is unexpected. We update the // connected (SOURCE_ADDED) or present (SOURCE_PRESENT). Any other reason
// preference with this source and won't change it's state. // is unexpected. We update the preference with this source and won't
// change it's state.
existingPreference.setAudioStreamMetadata(source); existingPreference.setAudioStreamMetadata(source);
if (fromState != AudioStreamState.SOURCE_ADDED) { if (fromState != AudioStreamState.SOURCE_ADDED
&& (!audioSharingHysteresisModeFix()
|| fromState != AudioStreamState.SOURCE_PRESENT)) {
Log.w( Log.w(
TAG, TAG,
"handleSourceFound(): unexpected state : " "handleSourceFound(): unexpected state : "
@@ -346,10 +359,14 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
for (var entry : mBroadcastIdToPreferenceMap.entrySet()) { for (var entry : mBroadcastIdToPreferenceMap.entrySet()) {
var preference = entry.getValue(); 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 // not, means the source is removed from the sink, we move back the preference to SYNCED
// state. // state.
if (preference.getAudioStreamState() == AudioStreamState.SOURCE_ADDED if ((preference.getAudioStreamState() == AudioStreamState.SOURCE_ADDED
|| (audioSharingHysteresisModeFix()
&& preference.getAudioStreamState()
== AudioStreamState.SOURCE_PRESENT))
&& mAudioStreamsHelper.getAllConnectedSources().stream() && mAudioStreamsHelper.getAllConnectedSources().stream()
.noneMatch( .noneMatch(
connected -> connected ->
@@ -383,6 +400,7 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
if (!AudioStreamsHelper.isConnected(receiveState)) { if (!AudioStreamsHelper.isConnected(receiveState)) {
return; return;
} }
var broadcastIdConnected = receiveState.getBroadcastId(); var broadcastIdConnected = receiveState.getBroadcastId();
if (mSourceFromQrCode != null && mSourceFromQrCode.getBroadcastId() == UNSET_BROADCAST_ID) { if (mSourceFromQrCode != null && mSourceFromQrCode.getBroadcastId() == UNSET_BROADCAST_ID) {
// mSourceFromQrCode could have no broadcast Id, we fill in the broadcast Id from the // 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. // Find preference by metadata and decide next state.
// Expect one preference existed, move to ADD_SOURCE_WAIT_FOR_RESPONSE // Expect one preference existed, move to ADD_SOURCE_WAIT_FOR_RESPONSE
void handleSourceAddRequest( void handleSourceAddRequest(
@@ -530,9 +600,23 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
// Handle QR code scan, display currently connected streams then start scanning // Handle QR code scan, display currently connected streams then start scanning
// sequentially // sequentially
handleSourceFromQrCodeIfExists(); handleSourceFromQrCodeIfExists();
mAudioStreamsHelper if (audioSharingHysteresisModeFix()) {
.getAllConnectedSources() // With hysteresis mode, we prioritize showing connected sources first.
.forEach(this::handleSourceConnected); // 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()); mLeBroadcastAssistant.startSearchingForSources(emptyList());
mMediaControlHelper.start(); mMediaControlHelper.start();
}); });
@@ -581,6 +665,7 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
AddSourceWaitForResponseState.getInstance(); AddSourceWaitForResponseState.getInstance();
case ADD_SOURCE_BAD_CODE -> AddSourceBadCodeState.getInstance(); case ADD_SOURCE_BAD_CODE -> AddSourceBadCodeState.getInstance();
case ADD_SOURCE_FAILED -> AddSourceFailedState.getInstance(); case ADD_SOURCE_FAILED -> AddSourceFailedState.getInstance();
case SOURCE_PRESENT -> SourcePresentState.getInstance();
case SOURCE_ADDED -> SourceAddedState.getInstance(); case SOURCE_ADDED -> SourceAddedState.getInstance();
default -> throw new IllegalArgumentException("Unsupported state: " + state); 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;
}
}

View File

@@ -16,6 +16,8 @@
package com.android.settings.connecteddevice.audiosharing.audiostreams; package com.android.settings.connecteddevice.audiosharing.audiostreams;
import static com.android.settingslib.flags.Flags.FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
@@ -34,6 +36,7 @@ import android.bluetooth.BluetoothLeBroadcastAssistant;
import android.bluetooth.BluetoothLeBroadcastMetadata; import android.bluetooth.BluetoothLeBroadcastMetadata;
import android.bluetooth.BluetoothLeBroadcastReceiveState; import android.bluetooth.BluetoothLeBroadcastReceiveState;
import android.content.Context; import android.content.Context;
import android.platform.test.flag.junit.SetFlagsRule;
import android.view.View; import android.view.View;
import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.LifecycleOwner;
@@ -72,8 +75,8 @@ import java.util.concurrent.Executor;
ShadowAudioStreamsHelper.class, ShadowAudioStreamsHelper.class,
}) })
public class AudioStreamButtonControllerTest { public class AudioStreamButtonControllerTest {
@Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule(); @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
@Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
private static final String KEY = "audio_stream_button"; private static final String KEY = "audio_stream_button";
private static final int BROADCAST_ID = 1; private static final int BROADCAST_ID = 1;
private final Context mContext = ApplicationProvider.getApplicationContext(); private final Context mContext = ApplicationProvider.getApplicationContext();
@@ -83,6 +86,7 @@ public class AudioStreamButtonControllerTest {
@Mock private LocalBluetoothLeBroadcastAssistant mAssistant; @Mock private LocalBluetoothLeBroadcastAssistant mAssistant;
@Mock private AudioStreamsRepository mRepository; @Mock private AudioStreamsRepository mRepository;
@Mock private ActionButtonsPreference mPreference; @Mock private ActionButtonsPreference mPreference;
@Mock private BluetoothDevice mSourceDevice;
private Lifecycle mLifecycle; private Lifecycle mLifecycle;
private LifecycleOwner mLifecycleOwner; private LifecycleOwner mLifecycleOwner;
private FakeFeatureFactory mFeatureFactory; private FakeFeatureFactory mFeatureFactory;
@@ -90,6 +94,7 @@ public class AudioStreamButtonControllerTest {
@Before @Before
public void setUp() { public void setUp() {
mSetFlagsRule.disableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
ShadowAudioStreamsHelper.setUseMock(mAudioStreamsHelper); ShadowAudioStreamsHelper.setUseMock(mAudioStreamsHelper);
when(mAudioStreamsHelper.getLeBroadcastAssistant()).thenReturn(mAssistant); when(mAudioStreamsHelper.getLeBroadcastAssistant()).thenReturn(mAssistant);
mFeatureFactory = FakeFeatureFactory.setupForTest(); mFeatureFactory = FakeFeatureFactory.setupForTest();
@@ -254,6 +259,33 @@ public class AudioStreamButtonControllerTest {
.setButton1Icon(com.android.settings.R.drawable.ic_settings_close); .setButton1Icon(com.android.settings.R.drawable.ic_settings_close);
} }
@Test
public void testCallback_onReceiveStateChangedWithSourcePresent_updateButton() {
mSetFlagsRule.enableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
String address = "11:22:33:44:55:66";
BluetoothLeBroadcastReceiveState state = mock(BluetoothLeBroadcastReceiveState.class);
when(state.getBroadcastId()).thenReturn(BROADCAST_ID);
when(state.getSourceDevice()).thenReturn(mSourceDevice);
when(mSourceDevice.getAddress()).thenReturn(address);
List<Long> bisSyncState = new ArrayList<>();
when(state.getBisSyncState()).thenReturn(bisSyncState);
when(mAudioStreamsHelper.getAllPresentSources()).thenReturn(List.of(state));
mController.displayPreference(mScreen);
mController.mBroadcastAssistantCallback.onReceiveStateChanged(
mock(BluetoothDevice.class), /* sourceId= */ 0, state);
verify(mFeatureFactory.metricsFeatureProvider, never())
.action(any(), eq(SettingsEnums.ACTION_AUDIO_STREAM_JOIN_SUCCEED), anyInt());
// Called twice, once in displayPreference, the other one in callback
verify(mPreference, times(2)).setButton1Enabled(true);
verify(mPreference, times(2)).setButton1Text(R.string.audio_streams_disconnect);
verify(mPreference, times(2))
.setButton1Icon(com.android.settings.R.drawable.ic_settings_close);
}
@Test @Test
public void testCallback_onSourceAddFailed_updateButton() { public void testCallback_onSourceAddFailed_updateButton() {
when(mAudioStreamsHelper.getAllConnectedSources()).thenReturn(Collections.emptyList()); when(mAudioStreamsHelper.getAllConnectedSources()).thenReturn(Collections.emptyList());

View File

@@ -18,6 +18,8 @@ package com.android.settings.connecteddevice.audiosharing.audiostreams;
import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamHeaderController.AUDIO_STREAM_HEADER_LISTENING_NOW_SUMMARY; import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamHeaderController.AUDIO_STREAM_HEADER_LISTENING_NOW_SUMMARY;
import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamHeaderController.AUDIO_STREAM_HEADER_NOT_LISTENING_SUMMARY; import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamHeaderController.AUDIO_STREAM_HEADER_NOT_LISTENING_SUMMARY;
import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamHeaderController.AUDIO_STREAM_HEADER_PRESENT_NOW_SUMMARY;
import static com.android.settingslib.flags.Flags.FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
@@ -31,6 +33,7 @@ import android.bluetooth.BluetoothLeBroadcastAssistant;
import android.bluetooth.BluetoothLeBroadcastReceiveState; import android.bluetooth.BluetoothLeBroadcastReceiveState;
import android.content.Context; import android.content.Context;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.platform.test.flag.junit.SetFlagsRule;
import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.LifecycleOwner;
import androidx.preference.PreferenceScreen; import androidx.preference.PreferenceScreen;
@@ -68,8 +71,9 @@ import java.util.concurrent.Executor;
ShadowAudioStreamsHelper.class, ShadowAudioStreamsHelper.class,
}) })
public class AudioStreamHeaderControllerTest { public class AudioStreamHeaderControllerTest {
@Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule(); @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
@Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
private static final String KEY = "audio_stream_header"; private static final String KEY = "audio_stream_header";
private static final int BROADCAST_ID = 1; private static final int BROADCAST_ID = 1;
private static final String BROADCAST_NAME = "broadcast name"; private static final String BROADCAST_NAME = "broadcast name";
@@ -81,12 +85,15 @@ public class AudioStreamHeaderControllerTest {
@Mock private AudioStreamDetailsFragment mFragment; @Mock private AudioStreamDetailsFragment mFragment;
@Mock private LayoutPreference mPreference; @Mock private LayoutPreference mPreference;
@Mock private EntityHeaderController mHeaderController; @Mock private EntityHeaderController mHeaderController;
@Mock private BluetoothDevice mBluetoothDevice;
private Lifecycle mLifecycle; private Lifecycle mLifecycle;
private LifecycleOwner mLifecycleOwner; private LifecycleOwner mLifecycleOwner;
private AudioStreamHeaderController mController; private AudioStreamHeaderController mController;
@Before @Before
public void setUp() { public void setUp() {
mSetFlagsRule.disableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
ShadowEntityHeaderController.setUseMock(mHeaderController); ShadowEntityHeaderController.setUseMock(mHeaderController);
ShadowAudioStreamsHelper.setUseMock(mAudioStreamsHelper); ShadowAudioStreamsHelper.setUseMock(mAudioStreamsHelper);
when(mAudioStreamsHelper.getLeBroadcastAssistant()).thenReturn(mAssistant); when(mAudioStreamsHelper.getLeBroadcastAssistant()).thenReturn(mAssistant);
@@ -168,6 +175,44 @@ public class AudioStreamHeaderControllerTest {
verify(mScreen).addPreference(any()); verify(mScreen).addPreference(any());
} }
@Test
public void testDisplayPreference_sourcePresent_setSummary() {
mSetFlagsRule.enableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
String address = "11:22:33:44:55:66";
when(mBroadcastReceiveState.getBroadcastId()).thenReturn(BROADCAST_ID);
when(mBroadcastReceiveState.getSourceDevice()).thenReturn(mBluetoothDevice);
when(mBluetoothDevice.getAddress()).thenReturn(address);
List<Long> bisSyncState = new ArrayList<>();
when(mBroadcastReceiveState.getBisSyncState()).thenReturn(bisSyncState);
when(mAudioStreamsHelper.getAllPresentSources())
.thenReturn(List.of(mBroadcastReceiveState));
mController.displayPreference(mScreen);
verify(mHeaderController).setLabel(BROADCAST_NAME);
verify(mHeaderController).setIcon(any(Drawable.class));
verify(mHeaderController)
.setSummary(mContext.getString(AUDIO_STREAM_HEADER_PRESENT_NOW_SUMMARY));
verify(mHeaderController).done(true);
verify(mScreen).addPreference(any());
}
@Test
public void testDisplayPreference_sourceNotPresent_setSummary() {
mSetFlagsRule.enableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
when(mAudioStreamsHelper.getAllPresentSources()).thenReturn(Collections.emptyList());
mController.displayPreference(mScreen);
verify(mHeaderController).setLabel(BROADCAST_NAME);
verify(mHeaderController).setIcon(any(Drawable.class));
verify(mHeaderController).setSummary(AUDIO_STREAM_HEADER_NOT_LISTENING_SUMMARY);
verify(mHeaderController).done(true);
verify(mScreen).addPreference(any());
}
@Test @Test
public void testCallback_onSourceRemoved_updateButton() { public void testCallback_onSourceRemoved_updateButton() {
when(mAudioStreamsHelper.getAllConnectedSources()).thenReturn(Collections.emptyList()); when(mAudioStreamsHelper.getAllConnectedSources()).thenReturn(Collections.emptyList());
@@ -212,4 +257,25 @@ public class AudioStreamHeaderControllerTest {
.setSummary(mContext.getString(AUDIO_STREAM_HEADER_LISTENING_NOW_SUMMARY)); .setSummary(mContext.getString(AUDIO_STREAM_HEADER_LISTENING_NOW_SUMMARY));
verify(mHeaderController, times(2)).done(true); verify(mHeaderController, times(2)).done(true);
} }
@Test
public void testCallback_onReceiveStateChangedWithSourcePresent_updateButton() {
mSetFlagsRule.enableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
String address = "11:22:33:44:55:66";
when(mAudioStreamsHelper.getAllPresentSources())
.thenReturn(List.of(mBroadcastReceiveState));
when(mBroadcastReceiveState.getBroadcastId()).thenReturn(BROADCAST_ID);
when(mBroadcastReceiveState.getSourceDevice()).thenReturn(mBluetoothDevice);
when(mBluetoothDevice.getAddress()).thenReturn(address);
mController.displayPreference(mScreen);
mController.mBroadcastAssistantCallback.onReceiveStateChanged(
mock(BluetoothDevice.class), /* sourceId= */ 0, mBroadcastReceiveState);
// Called twice, once in displayPreference, the other one in callback
verify(mHeaderController, times(2))
.setSummary(mContext.getString(AUDIO_STREAM_HEADER_PRESENT_NOW_SUMMARY));
verify(mHeaderController, times(2)).done(true);
}
} }

View File

@@ -16,6 +16,8 @@
package com.android.settings.connecteddevice.audiosharing.audiostreams; package com.android.settings.connecteddevice.audiosharing.audiostreams;
import static com.android.settingslib.flags.Flags.FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
@@ -30,6 +32,7 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import android.content.Context; import android.content.Context;
import android.platform.test.flag.junit.SetFlagsRule;
import android.text.SpannableString; import android.text.SpannableString;
import androidx.preference.Preference; import androidx.preference.Preference;
@@ -48,6 +51,8 @@ import org.robolectric.RobolectricTestRunner;
@RunWith(RobolectricTestRunner.class) @RunWith(RobolectricTestRunner.class)
public class AudioStreamStateHandlerTest { public class AudioStreamStateHandlerTest {
@Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule(); @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
@Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
private static final int SUMMARY_RES = 1; private static final int SUMMARY_RES = 1;
private static final String SUMMARY = "summary"; private static final String SUMMARY = "summary";
private final Context mContext = spy(ApplicationProvider.getApplicationContext()); private final Context mContext = spy(ApplicationProvider.getApplicationContext());
@@ -58,6 +63,7 @@ public class AudioStreamStateHandlerTest {
@Before @Before
public void setUp() { public void setUp() {
mSetFlagsRule.disableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
mHandler = spy(new AudioStreamStateHandler()); mHandler = spy(new AudioStreamStateHandler());
} }
@@ -101,6 +107,28 @@ public class AudioStreamStateHandlerTest {
verify(mPreference).setOnPreferenceClickListener(eq(null)); verify(mPreference).setOnPreferenceClickListener(eq(null));
} }
@Test
public void testHandleStateChange_setNewState_sourcePresent() {
mSetFlagsRule.enableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
when(mHandler.getStateEnum())
.thenReturn(AudioStreamsProgressCategoryController.AudioStreamState.SOURCE_PRESENT);
when(mPreference.getAudioStreamState())
.thenReturn(
AudioStreamsProgressCategoryController.AudioStreamState
.ADD_SOURCE_BAD_CODE);
mHandler.handleStateChange(mPreference, mController, mHelper);
verify(mPreference)
.setAudioStreamState(
AudioStreamsProgressCategoryController.AudioStreamState.SOURCE_PRESENT);
verify(mHandler).performAction(any(), any(), any());
verify(mPreference).setIsConnected(eq(true));
verify(mPreference).setSummary(eq(""));
verify(mPreference).setOnPreferenceClickListener(eq(null));
}
@Test @Test
public void testHandleStateChange_setNewState_newSummary_newListener() { public void testHandleStateChange_setNewState_newSummary_newListener() {
Preference.OnPreferenceClickListener listener = Preference.OnPreferenceClickListener listener =

View File

@@ -19,6 +19,8 @@ package com.android.settings.connecteddevice.audiosharing.audiostreams;
import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
import static android.content.res.Configuration.ORIENTATION_PORTRAIT; import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
import static com.android.settingslib.flags.Flags.FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
@@ -37,6 +39,7 @@ import android.bluetooth.BluetoothLeBroadcastReceiveState;
import android.content.Context; import android.content.Context;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.content.res.Resources; import android.content.res.Resources;
import android.platform.test.flag.junit.SetFlagsRule;
import androidx.fragment.app.FragmentActivity; import androidx.fragment.app.FragmentActivity;
import androidx.test.core.app.ApplicationProvider; import androidx.test.core.app.ApplicationProvider;
@@ -74,6 +77,8 @@ import java.util.List;
}) })
public class AudioStreamsHelperTest { public class AudioStreamsHelperTest {
@Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule(); @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
@Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
private static final int GROUP_ID = 1; private static final int GROUP_ID = 1;
private static final int BROADCAST_ID_1 = 1; private static final int BROADCAST_ID_1 = 1;
private static final int BROADCAST_ID_2 = 2; private static final int BROADCAST_ID_2 = 2;
@@ -86,10 +91,12 @@ public class AudioStreamsHelperTest {
@Mock private BluetoothLeBroadcastMetadata mMetadata; @Mock private BluetoothLeBroadcastMetadata mMetadata;
@Mock private CachedBluetoothDevice mCachedDevice; @Mock private CachedBluetoothDevice mCachedDevice;
@Mock private BluetoothDevice mDevice; @Mock private BluetoothDevice mDevice;
@Mock private BluetoothDevice mSourceDevice;
private AudioStreamsHelper mHelper; private AudioStreamsHelper mHelper;
@Before @Before
public void setUp() { public void setUp() {
mSetFlagsRule.disableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
when(mLocalBluetoothManager.getProfileManager()).thenReturn(mLocalBluetoothProfileManager); when(mLocalBluetoothManager.getProfileManager()).thenReturn(mLocalBluetoothProfileManager);
when(mLocalBluetoothManager.getCachedDeviceManager()).thenReturn(mDeviceManager); when(mLocalBluetoothManager.getCachedDeviceManager()).thenReturn(mDeviceManager);
when(mLocalBluetoothProfileManager.getLeAudioBroadcastAssistantProfile()) when(mLocalBluetoothProfileManager.getLeAudioBroadcastAssistantProfile())
@@ -166,6 +173,7 @@ public class AudioStreamsHelperTest {
@Test @Test
public void removeSource_memberHasConnectedSource() { public void removeSource_memberHasConnectedSource() {
String address = "11:22:33:44:55:66";
List<BluetoothDevice> devices = new ArrayList<>(); List<BluetoothDevice> devices = new ArrayList<>();
var memberDevice = mock(BluetoothDevice.class); var memberDevice = mock(BluetoothDevice.class);
devices.add(mDevice); devices.add(mDevice);
@@ -184,6 +192,8 @@ public class AudioStreamsHelperTest {
List<Long> bisSyncState = new ArrayList<>(); List<Long> bisSyncState = new ArrayList<>();
bisSyncState.add(1L); bisSyncState.add(1L);
when(source.getBisSyncState()).thenReturn(bisSyncState); when(source.getBisSyncState()).thenReturn(bisSyncState);
when(source.getSourceDevice()).thenReturn(mSourceDevice);
when(mSourceDevice.getAddress()).thenReturn(address);
mHelper.removeSource(BROADCAST_ID_2); mHelper.removeSource(BROADCAST_ID_2);
@@ -217,6 +227,52 @@ public class AudioStreamsHelperTest {
assertThat(list.get(0)).isEqualTo(source); assertThat(list.get(0)).isEqualTo(source);
} }
@Test
public void getAllPresentSources_noSource() {
mSetFlagsRule.enableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
List<BluetoothDevice> devices = new ArrayList<>();
devices.add(mDevice);
String address = "00:00:00:00:00:00";
when(mAssistant.getAllConnectedDevices()).thenReturn(devices);
BluetoothLeBroadcastReceiveState source = mock(BluetoothLeBroadcastReceiveState.class);
when(mDeviceManager.findDevice(any())).thenReturn(mCachedDevice);
when(mCachedDevice.getDevice()).thenReturn(mDevice);
when(mCachedDevice.getGroupId()).thenReturn(GROUP_ID);
when(mAssistant.getAllSources(any())).thenReturn(ImmutableList.of(source));
when(source.getSourceDevice()).thenReturn(mSourceDevice);
when(mSourceDevice.getAddress()).thenReturn(address);
var list = mHelper.getAllPresentSources();
assertThat(list).isEmpty();
}
@Test
public void getAllPresentSources_returnSource() {
mSetFlagsRule.enableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
String address = "11:22:33:44:55:66";
List<BluetoothDevice> devices = new ArrayList<>();
devices.add(mDevice);
when(mAssistant.getAllConnectedDevices()).thenReturn(devices);
BluetoothLeBroadcastReceiveState source = mock(BluetoothLeBroadcastReceiveState.class);
when(mDeviceManager.findDevice(any())).thenReturn(mCachedDevice);
when(mCachedDevice.getDevice()).thenReturn(mDevice);
when(mCachedDevice.getGroupId()).thenReturn(GROUP_ID);
when(mAssistant.getAllSources(any())).thenReturn(ImmutableList.of(source));
when(source.getSourceDevice()).thenReturn(mSourceDevice);
when(mSourceDevice.getAddress()).thenReturn(address);
List<Long> bisSyncState = new ArrayList<>();
when(source.getBisSyncState()).thenReturn(bisSyncState);
var list = mHelper.getAllPresentSources();
assertThat(list).isNotEmpty();
assertThat(list.get(0)).isEqualTo(source);
}
@Test @Test
public void startMediaService_noDevice_doNothing() { public void startMediaService_noDevice_doNothing() {
mHelper.startMediaService(mContext, BROADCAST_ID_1, BROADCAST_NAME); mHelper.startMediaService(mContext, BROADCAST_ID_1, BROADCAST_NAME);

View File

@@ -16,6 +16,8 @@
package com.android.settings.connecteddevice.audiosharing.audiostreams; package com.android.settings.connecteddevice.audiosharing.audiostreams;
import static com.android.settingslib.flags.Flags.FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.anyString;
@@ -25,6 +27,7 @@ import static org.mockito.Mockito.when;
import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothLeBroadcastMetadata; import android.bluetooth.BluetoothLeBroadcastMetadata;
import android.bluetooth.BluetoothLeBroadcastReceiveState; import android.bluetooth.BluetoothLeBroadcastReceiveState;
import android.platform.test.flag.junit.SetFlagsRule;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule; import org.junit.Rule;
@@ -41,14 +44,18 @@ import java.util.List;
@RunWith(RobolectricTestRunner.class) @RunWith(RobolectricTestRunner.class)
public class AudioStreamsProgressCategoryCallbackTest { public class AudioStreamsProgressCategoryCallbackTest {
@Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule(); @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
@Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
@Mock private AudioStreamsProgressCategoryController mController; @Mock private AudioStreamsProgressCategoryController mController;
@Mock private BluetoothDevice mDevice; @Mock private BluetoothDevice mDevice;
@Mock private BluetoothLeBroadcastReceiveState mState; @Mock private BluetoothLeBroadcastReceiveState mState;
@Mock private BluetoothLeBroadcastMetadata mMetadata; @Mock private BluetoothLeBroadcastMetadata mMetadata;
@Mock private BluetoothDevice mSourceDevice;
private AudioStreamsProgressCategoryCallback mCallback; private AudioStreamsProgressCategoryCallback mCallback;
@Before @Before
public void setUp() { public void setUp() {
mSetFlagsRule.disableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
mCallback = new AudioStreamsProgressCategoryCallback(mController); mCallback = new AudioStreamsProgressCategoryCallback(mController);
} }
@@ -62,6 +69,20 @@ public class AudioStreamsProgressCategoryCallbackTest {
verify(mController).handleSourceConnected(any()); verify(mController).handleSourceConnected(any());
} }
@Test
public void testOnReceiveStateChanged_sourcePresent() {
mSetFlagsRule.enableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
String address = "11:22:33:44:55:66";
List<Long> bisSyncState = new ArrayList<>();
when(mState.getBisSyncState()).thenReturn(bisSyncState);
when(mState.getSourceDevice()).thenReturn(mSourceDevice);
when(mSourceDevice.getAddress()).thenReturn(address);
mCallback.onReceiveStateChanged(mDevice, /* sourceId= */ 0, mState);
verify(mController).handleSourcePresent(any());
}
@Test @Test
public void testOnReceiveStateChanged_badCode() { public void testOnReceiveStateChanged_badCode() {
when(mState.getPaSyncState()) when(mState.getPaSyncState())

View File

@@ -20,10 +20,12 @@ import static com.android.settings.connecteddevice.audiosharing.audiostreams.Aud
import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsProgressCategoryController.AudioStreamState.ADD_SOURCE_FAILED; import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsProgressCategoryController.AudioStreamState.ADD_SOURCE_FAILED;
import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsProgressCategoryController.AudioStreamState.ADD_SOURCE_WAIT_FOR_RESPONSE; import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsProgressCategoryController.AudioStreamState.ADD_SOURCE_WAIT_FOR_RESPONSE;
import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsProgressCategoryController.AudioStreamState.SOURCE_ADDED; import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsProgressCategoryController.AudioStreamState.SOURCE_ADDED;
import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsProgressCategoryController.AudioStreamState.SOURCE_PRESENT;
import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsProgressCategoryController.AudioStreamState.SYNCED; import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsProgressCategoryController.AudioStreamState.SYNCED;
import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsProgressCategoryController.AudioStreamState.WAIT_FOR_SYNC; import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsProgressCategoryController.AudioStreamState.WAIT_FOR_SYNC;
import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsProgressCategoryController.UNSET_BROADCAST_ID; import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsProgressCategoryController.UNSET_BROADCAST_ID;
import static com.android.settings.core.BasePreferenceController.AVAILABLE; import static com.android.settings.core.BasePreferenceController.AVAILABLE;
import static com.android.settingslib.flags.Flags.FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
@@ -41,12 +43,14 @@ import static org.robolectric.Shadows.shadowOf;
import static java.util.Collections.emptyList; import static java.util.Collections.emptyList;
import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothLeAudioContentMetadata; import android.bluetooth.BluetoothLeAudioContentMetadata;
import android.bluetooth.BluetoothLeBroadcastMetadata; import android.bluetooth.BluetoothLeBroadcastMetadata;
import android.bluetooth.BluetoothLeBroadcastReceiveState; import android.bluetooth.BluetoothLeBroadcastReceiveState;
import android.bluetooth.BluetoothProfile; import android.bluetooth.BluetoothProfile;
import android.content.Context; import android.content.Context;
import android.os.Looper; import android.os.Looper;
import android.platform.test.flag.junit.SetFlagsRule;
import android.view.View; import android.view.View;
import android.widget.Button; import android.widget.Button;
import android.widget.TextView; import android.widget.TextView;
@@ -96,6 +100,8 @@ import java.util.List;
}) })
public class AudioStreamsProgressCategoryControllerTest { public class AudioStreamsProgressCategoryControllerTest {
@Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule(); @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
@Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
private static final String VALID_METADATA = private static final String VALID_METADATA =
"BLUETOOTH:UUID:184F;BN:VGVzdA==;AT:1;AD:00A1A1A1A1A1;BI:1E240;BC:VGVzdENvZGU=;" "BLUETOOTH:UUID:184F;BN:VGVzdA==;AT:1;AD:00A1A1A1A1A1;BI:1E240;BC:VGVzdENvZGU=;"
+ "MD:BgNwVGVzdA==;AS:1;PI:A0;NS:1;BS:3;NB:2;SM:BQNUZXN0BARlbmc=;;"; + "MD:BgNwVGVzdA==;AS:1;PI:A0;NS:1;BS:3;NB:2;SM:BQNUZXN0BARlbmc=;;";
@@ -115,6 +121,7 @@ public class AudioStreamsProgressCategoryControllerTest {
@Mock private BluetoothLeBroadcastMetadata mMetadata; @Mock private BluetoothLeBroadcastMetadata mMetadata;
@Mock private CachedBluetoothDevice mDevice; @Mock private CachedBluetoothDevice mDevice;
@Mock private AudioStreamsProgressCategoryPreference mPreference; @Mock private AudioStreamsProgressCategoryPreference mPreference;
@Mock private BluetoothDevice mSourceDevice;
private Lifecycle mLifecycle; private Lifecycle mLifecycle;
private LifecycleOwner mLifecycleOwner; private LifecycleOwner mLifecycleOwner;
private Fragment mFragment; private Fragment mFragment;
@@ -125,6 +132,7 @@ public class AudioStreamsProgressCategoryControllerTest {
ShadowAudioStreamsHelper.setUseMock(mAudioStreamsHelper); ShadowAudioStreamsHelper.setUseMock(mAudioStreamsHelper);
when(mAudioStreamsHelper.getLeBroadcastAssistant()).thenReturn(mLeBroadcastAssistant); when(mAudioStreamsHelper.getLeBroadcastAssistant()).thenReturn(mLeBroadcastAssistant);
when(mAudioStreamsHelper.getAllConnectedSources()).thenReturn(emptyList()); when(mAudioStreamsHelper.getAllConnectedSources()).thenReturn(emptyList());
mSetFlagsRule.disableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
ShadowBluetoothUtils.sLocalBluetoothManager = mLocalBtManager; ShadowBluetoothUtils.sLocalBluetoothManager = mLocalBtManager;
when(mLocalBtManager.getEventManager()).thenReturn(mBluetoothEventManager); when(mLocalBtManager.getEventManager()).thenReturn(mBluetoothEventManager);
@@ -282,6 +290,29 @@ public class AudioStreamsProgressCategoryControllerTest {
verify(mController, never()).moveToState(any(), any()); verify(mController, never()).moveToState(any(), any());
} }
@Test
public void testOnStart_initHasDevice_getPresentSources() {
mSetFlagsRule.enableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
// Setup a device
ShadowAudioStreamsHelper.setCachedBluetoothDeviceInSharingOrLeConnected(mDevice);
List<BluetoothLeBroadcastReceiveState> connectedList = new ArrayList<>();
// Empty connected device list
when(mAudioStreamsHelper.getAllConnectedSources()).thenReturn(connectedList);
mController.onStart(mLifecycleOwner);
shadowOf(Looper.getMainLooper()).idle();
verify(mAudioStreamsHelper).getAllPresentSources();
verify(mLeBroadcastAssistant).startSearchingForSources(any());
var dialog = ShadowAlertDialog.getLatestAlertDialog();
assertThat(dialog).isNull();
verify(mController, never()).moveToState(any(), any());
}
@Test @Test
public void testOnStart_handleSourceFromQrCode() { public void testOnStart_handleSourceFromQrCode() {
// Setup a device // Setup a device
@@ -764,6 +795,58 @@ public class AudioStreamsProgressCategoryControllerTest {
assertThat(states.get(1)).isEqualTo(ADD_SOURCE_FAILED); assertThat(states.get(1)).isEqualTo(ADD_SOURCE_FAILED);
} }
@Test
public void testHandleSourcePresent_updateState() {
mSetFlagsRule.enableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
String address = "11:22:33:44:55:66";
// Setup a device
ShadowAudioStreamsHelper.setCachedBluetoothDeviceInSharingOrLeConnected(mDevice);
// Setup mPreference so it's not null
mController.displayPreference(mScreen);
// A new source found
when(mMetadata.getBroadcastId()).thenReturn(NEWLY_FOUND_BROADCAST_ID);
mController.handleSourceFound(mMetadata);
shadowOf(Looper.getMainLooper()).idle();
// The connected source is identified as having a bad code
BluetoothLeBroadcastReceiveState receiveState =
mock(BluetoothLeBroadcastReceiveState.class);
when(receiveState.getBroadcastId()).thenReturn(NEWLY_FOUND_BROADCAST_ID);
when(receiveState.getSourceDevice()).thenReturn(mSourceDevice);
when(mSourceDevice.getAddress()).thenReturn(address);
List<Long> bisSyncState = new ArrayList<>();
when(receiveState.getBisSyncState()).thenReturn(bisSyncState);
// The new found source is identified as failed to connect
mController.handleSourcePresent(receiveState);
shadowOf(Looper.getMainLooper()).idle();
ArgumentCaptor<AudioStreamPreference> preference =
ArgumentCaptor.forClass(AudioStreamPreference.class);
ArgumentCaptor<AudioStreamsProgressCategoryController.AudioStreamState> state =
ArgumentCaptor.forClass(
AudioStreamsProgressCategoryController.AudioStreamState.class);
verify(mController, times(2)).moveToState(preference.capture(), state.capture());
List<AudioStreamPreference> preferences = preference.getAllValues();
assertThat(preferences.size()).isEqualTo(2);
List<AudioStreamsProgressCategoryController.AudioStreamState> states = state.getAllValues();
assertThat(states.size()).isEqualTo(2);
// Verify one preference is created with SYNCED
assertThat(preferences.get(0).getAudioStreamBroadcastId())
.isEqualTo(NEWLY_FOUND_BROADCAST_ID);
assertThat(states.get(0)).isEqualTo(SYNCED);
// Verify the preference is updated to state ADD_SOURCE_FAILED
assertThat(preferences.get(1).getAudioStreamBroadcastId())
.isEqualTo(NEWLY_FOUND_BROADCAST_ID);
assertThat(states.get(1)).isEqualTo(SOURCE_PRESENT);
}
private static BluetoothLeBroadcastReceiveState createConnectedMock(int id) { private static BluetoothLeBroadcastReceiveState createConnectedMock(int id) {
var connected = mock(BluetoothLeBroadcastReceiveState.class); var connected = mock(BluetoothLeBroadcastReceiveState.class);
List<Long> bisSyncState = new ArrayList<>(); List<Long> bisSyncState = new ArrayList<>();

View File

@@ -0,0 +1,137 @@
/*
* 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 static android.app.settings.SettingsEnums.AUDIO_STREAM_MAIN;
import static com.android.settings.connecteddevice.audiosharing.audiostreams.SourcePresentState.AUDIO_STREAM_SOURCE_PRESENT_STATE_SUMMARY;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import androidx.fragment.app.FragmentActivity;
import androidx.preference.Preference;
import androidx.test.core.app.ApplicationProvider;
import com.android.settings.R;
import com.android.settings.SettingsActivity;
import com.android.settings.testutils.FakeFeatureFactory;
import com.android.settings.testutils.shadow.ShadowFragment;
import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
@RunWith(RobolectricTestRunner.class)
@Config(
shadows = {
ShadowFragment.class,
})
public class SourcePresentStateTest {
@Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
private static final int BROADCAST_ID = 1;
private static final String BROADCAST_TITLE = "title";
private final Context mContext = ApplicationProvider.getApplicationContext();
@Mock private AudioStreamPreference mPreference;
@Mock private AudioStreamsProgressCategoryController mController;
@Mock private AudioStreamsHelper mHelper;
@Mock private AudioStreamsRepository mRepository;
@Mock private AudioStreamsDashboardFragment mFragment;
@Mock private FragmentActivity mActivity;
private FakeFeatureFactory mFeatureFactory;
private SourcePresentState mInstance;
@Before
public void setUp() {
when(mFragment.getActivity()).thenReturn(mActivity);
mFeatureFactory = FakeFeatureFactory.setupForTest();
mInstance = new SourcePresentState();
when(mPreference.getAudioStreamBroadcastId()).thenReturn(BROADCAST_ID);
when(mPreference.getTitle()).thenReturn(BROADCAST_TITLE);
}
@Test
public void testGetInstance() {
mInstance = SourcePresentState.getInstance();
assertThat(mInstance).isNotNull();
assertThat(mInstance).isInstanceOf(SourcePresentState.class);
}
@Test
public void testGetSummary() {
int summary = mInstance.getSummary();
assertThat(summary).isEqualTo(AUDIO_STREAM_SOURCE_PRESENT_STATE_SUMMARY);
}
@Test
public void testGetStateEnum() {
AudioStreamsProgressCategoryController.AudioStreamState stateEnum =
mInstance.getStateEnum();
assertThat(stateEnum)
.isEqualTo(AudioStreamsProgressCategoryController.AudioStreamState.SOURCE_PRESENT);
}
@Test
public void testGetOnClickListener_startSubSettings() {
when(mController.getFragment()).thenReturn(mFragment);
when(mFragment.getMetricsCategory()).thenReturn(AUDIO_STREAM_MAIN);
Preference.OnPreferenceClickListener listener = mInstance.getOnClickListener(mController);
assertThat(listener).isNotNull();
// mContext is not an Activity context, calling startActivity() from outside of an Activity
// context requires the FLAG_ACTIVITY_NEW_TASK flag, create a mock to avoid this
// AndroidRuntimeException.
Context activityContext = mock(Context.class);
when(mPreference.getContext()).thenReturn(activityContext);
listener.onPreferenceClick(mPreference);
ArgumentCaptor<Intent> argumentCaptor = ArgumentCaptor.forClass(Intent.class);
verify(activityContext).startActivity(argumentCaptor.capture());
Intent intent = argumentCaptor.getValue();
assertThat(intent.getStringExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT))
.isEqualTo(AudioStreamDetailsFragment.class.getName());
assertThat(intent.getIntExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_TITLE_RESID, 0))
.isEqualTo(R.string.audio_streams_detail_page_title);
assertThat(intent.getIntExtra(MetricsFeatureProvider.EXTRA_SOURCE_METRICS_CATEGORY, 0))
.isEqualTo(AUDIO_STREAM_MAIN);
Bundle bundle = intent.getBundleExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS);
assertThat(bundle).isNotNull();
assertThat(bundle.getString(AudioStreamDetailsFragment.BROADCAST_NAME_ARG))
.isEqualTo(BROADCAST_TITLE);
assertThat(bundle.getInt(AudioStreamDetailsFragment.BROADCAST_ID_ARG))
.isEqualTo(BROADCAST_ID);
}
}

View File

@@ -59,6 +59,11 @@ public class ShadowAudioStreamsHelper {
return sMockHelper.getAllConnectedSources(); return sMockHelper.getAllConnectedSources();
} }
@Implementation
public List<BluetoothLeBroadcastReceiveState> getAllPresentSources() {
return sMockHelper.getAllPresentSources();
}
/** Gets {@link CachedBluetoothDevice} in sharing or le connected */ /** Gets {@link CachedBluetoothDevice} in sharing or le connected */
@Implementation @Implementation
public static Optional<CachedBluetoothDevice> getCachedBluetoothDeviceInSharingOrLeConnected( public static Optional<CachedBluetoothDevice> getCachedBluetoothDeviceInSharingOrLeConnected(