[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:
@@ -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>
|
||||
<!-- The preference summary when add source succeed [CHAR LIMIT=NONE] -->
|
||||
<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] -->
|
||||
<string name="audio_streams_media_service_notification_leave_broadcast_text">Stop listening</string>
|
||||
<!-- Le audio streams no le device dialog title [CHAR LIMIT=NONE] -->
|
||||
|
@@ -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);
|
||||
|
||||
|
@@ -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) {
|
||||
|
@@ -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));
|
||||
});
|
||||
}
|
||||
|
@@ -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 = "
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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);
|
||||
};
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -16,6 +16,8 @@
|
||||
|
||||
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 org.mockito.ArgumentMatchers.any;
|
||||
@@ -34,6 +36,7 @@ import android.bluetooth.BluetoothLeBroadcastAssistant;
|
||||
import android.bluetooth.BluetoothLeBroadcastMetadata;
|
||||
import android.bluetooth.BluetoothLeBroadcastReceiveState;
|
||||
import android.content.Context;
|
||||
import android.platform.test.flag.junit.SetFlagsRule;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
@@ -72,8 +75,8 @@ import java.util.concurrent.Executor;
|
||||
ShadowAudioStreamsHelper.class,
|
||||
})
|
||||
public class AudioStreamButtonControllerTest {
|
||||
|
||||
@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 int BROADCAST_ID = 1;
|
||||
private final Context mContext = ApplicationProvider.getApplicationContext();
|
||||
@@ -83,6 +86,7 @@ public class AudioStreamButtonControllerTest {
|
||||
@Mock private LocalBluetoothLeBroadcastAssistant mAssistant;
|
||||
@Mock private AudioStreamsRepository mRepository;
|
||||
@Mock private ActionButtonsPreference mPreference;
|
||||
@Mock private BluetoothDevice mSourceDevice;
|
||||
private Lifecycle mLifecycle;
|
||||
private LifecycleOwner mLifecycleOwner;
|
||||
private FakeFeatureFactory mFeatureFactory;
|
||||
@@ -90,6 +94,7 @@ public class AudioStreamButtonControllerTest {
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
mSetFlagsRule.disableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
|
||||
ShadowAudioStreamsHelper.setUseMock(mAudioStreamsHelper);
|
||||
when(mAudioStreamsHelper.getLeBroadcastAssistant()).thenReturn(mAssistant);
|
||||
mFeatureFactory = FakeFeatureFactory.setupForTest();
|
||||
@@ -254,6 +259,33 @@ public class AudioStreamButtonControllerTest {
|
||||
.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
|
||||
public void testCallback_onSourceAddFailed_updateButton() {
|
||||
when(mAudioStreamsHelper.getAllConnectedSources()).thenReturn(Collections.emptyList());
|
||||
|
@@ -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_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.Mockito.mock;
|
||||
@@ -31,6 +33,7 @@ import android.bluetooth.BluetoothLeBroadcastAssistant;
|
||||
import android.bluetooth.BluetoothLeBroadcastReceiveState;
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.platform.test.flag.junit.SetFlagsRule;
|
||||
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.preference.PreferenceScreen;
|
||||
@@ -68,8 +71,9 @@ import java.util.concurrent.Executor;
|
||||
ShadowAudioStreamsHelper.class,
|
||||
})
|
||||
public class AudioStreamHeaderControllerTest {
|
||||
|
||||
@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 int BROADCAST_ID = 1;
|
||||
private static final String BROADCAST_NAME = "broadcast name";
|
||||
@@ -81,12 +85,15 @@ public class AudioStreamHeaderControllerTest {
|
||||
@Mock private AudioStreamDetailsFragment mFragment;
|
||||
@Mock private LayoutPreference mPreference;
|
||||
@Mock private EntityHeaderController mHeaderController;
|
||||
@Mock private BluetoothDevice mBluetoothDevice;
|
||||
private Lifecycle mLifecycle;
|
||||
private LifecycleOwner mLifecycleOwner;
|
||||
private AudioStreamHeaderController mController;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
mSetFlagsRule.disableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
|
||||
|
||||
ShadowEntityHeaderController.setUseMock(mHeaderController);
|
||||
ShadowAudioStreamsHelper.setUseMock(mAudioStreamsHelper);
|
||||
when(mAudioStreamsHelper.getLeBroadcastAssistant()).thenReturn(mAssistant);
|
||||
@@ -168,6 +175,44 @@ public class AudioStreamHeaderControllerTest {
|
||||
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
|
||||
public void testCallback_onSourceRemoved_updateButton() {
|
||||
when(mAudioStreamsHelper.getAllConnectedSources()).thenReturn(Collections.emptyList());
|
||||
@@ -212,4 +257,25 @@ public class AudioStreamHeaderControllerTest {
|
||||
.setSummary(mContext.getString(AUDIO_STREAM_HEADER_LISTENING_NOW_SUMMARY));
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@@ -16,6 +16,8 @@
|
||||
|
||||
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 org.mockito.ArgumentMatchers.any;
|
||||
@@ -30,6 +32,7 @@ import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import android.content.Context;
|
||||
import android.platform.test.flag.junit.SetFlagsRule;
|
||||
import android.text.SpannableString;
|
||||
|
||||
import androidx.preference.Preference;
|
||||
@@ -48,6 +51,8 @@ import org.robolectric.RobolectricTestRunner;
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
public class AudioStreamStateHandlerTest {
|
||||
@Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
|
||||
@Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
|
||||
|
||||
private static final int SUMMARY_RES = 1;
|
||||
private static final String SUMMARY = "summary";
|
||||
private final Context mContext = spy(ApplicationProvider.getApplicationContext());
|
||||
@@ -58,6 +63,7 @@ public class AudioStreamStateHandlerTest {
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
mSetFlagsRule.disableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
|
||||
mHandler = spy(new AudioStreamStateHandler());
|
||||
}
|
||||
|
||||
@@ -101,6 +107,28 @@ public class AudioStreamStateHandlerTest {
|
||||
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
|
||||
public void testHandleStateChange_setNewState_newSummary_newListener() {
|
||||
Preference.OnPreferenceClickListener listener =
|
||||
|
@@ -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_PORTRAIT;
|
||||
|
||||
import static com.android.settingslib.flags.Flags.FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
@@ -37,6 +39,7 @@ import android.bluetooth.BluetoothLeBroadcastReceiveState;
|
||||
import android.content.Context;
|
||||
import android.content.res.Configuration;
|
||||
import android.content.res.Resources;
|
||||
import android.platform.test.flag.junit.SetFlagsRule;
|
||||
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
@@ -74,6 +77,8 @@ import java.util.List;
|
||||
})
|
||||
public class AudioStreamsHelperTest {
|
||||
@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 BROADCAST_ID_1 = 1;
|
||||
private static final int BROADCAST_ID_2 = 2;
|
||||
@@ -86,10 +91,12 @@ public class AudioStreamsHelperTest {
|
||||
@Mock private BluetoothLeBroadcastMetadata mMetadata;
|
||||
@Mock private CachedBluetoothDevice mCachedDevice;
|
||||
@Mock private BluetoothDevice mDevice;
|
||||
@Mock private BluetoothDevice mSourceDevice;
|
||||
private AudioStreamsHelper mHelper;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
mSetFlagsRule.disableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
|
||||
when(mLocalBluetoothManager.getProfileManager()).thenReturn(mLocalBluetoothProfileManager);
|
||||
when(mLocalBluetoothManager.getCachedDeviceManager()).thenReturn(mDeviceManager);
|
||||
when(mLocalBluetoothProfileManager.getLeAudioBroadcastAssistantProfile())
|
||||
@@ -166,6 +173,7 @@ public class AudioStreamsHelperTest {
|
||||
|
||||
@Test
|
||||
public void removeSource_memberHasConnectedSource() {
|
||||
String address = "11:22:33:44:55:66";
|
||||
List<BluetoothDevice> devices = new ArrayList<>();
|
||||
var memberDevice = mock(BluetoothDevice.class);
|
||||
devices.add(mDevice);
|
||||
@@ -184,6 +192,8 @@ public class AudioStreamsHelperTest {
|
||||
List<Long> bisSyncState = new ArrayList<>();
|
||||
bisSyncState.add(1L);
|
||||
when(source.getBisSyncState()).thenReturn(bisSyncState);
|
||||
when(source.getSourceDevice()).thenReturn(mSourceDevice);
|
||||
when(mSourceDevice.getAddress()).thenReturn(address);
|
||||
|
||||
mHelper.removeSource(BROADCAST_ID_2);
|
||||
|
||||
@@ -217,6 +227,52 @@ public class AudioStreamsHelperTest {
|
||||
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
|
||||
public void startMediaService_noDevice_doNothing() {
|
||||
mHelper.startMediaService(mContext, BROADCAST_ID_1, BROADCAST_NAME);
|
||||
|
@@ -16,6 +16,8 @@
|
||||
|
||||
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.anyBoolean;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
@@ -25,6 +27,7 @@ import static org.mockito.Mockito.when;
|
||||
import android.bluetooth.BluetoothDevice;
|
||||
import android.bluetooth.BluetoothLeBroadcastMetadata;
|
||||
import android.bluetooth.BluetoothLeBroadcastReceiveState;
|
||||
import android.platform.test.flag.junit.SetFlagsRule;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
@@ -41,14 +44,18 @@ import java.util.List;
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
public class AudioStreamsProgressCategoryCallbackTest {
|
||||
@Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
|
||||
@Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
|
||||
|
||||
@Mock private AudioStreamsProgressCategoryController mController;
|
||||
@Mock private BluetoothDevice mDevice;
|
||||
@Mock private BluetoothLeBroadcastReceiveState mState;
|
||||
@Mock private BluetoothLeBroadcastMetadata mMetadata;
|
||||
@Mock private BluetoothDevice mSourceDevice;
|
||||
private AudioStreamsProgressCategoryCallback mCallback;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
mSetFlagsRule.disableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
|
||||
mCallback = new AudioStreamsProgressCategoryCallback(mController);
|
||||
}
|
||||
|
||||
@@ -62,6 +69,20 @@ public class AudioStreamsProgressCategoryCallbackTest {
|
||||
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
|
||||
public void testOnReceiveStateChanged_badCode() {
|
||||
when(mState.getPaSyncState())
|
||||
|
@@ -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_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_PRESENT;
|
||||
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.UNSET_BROADCAST_ID;
|
||||
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;
|
||||
|
||||
@@ -41,12 +43,14 @@ import static org.robolectric.Shadows.shadowOf;
|
||||
import static java.util.Collections.emptyList;
|
||||
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.bluetooth.BluetoothDevice;
|
||||
import android.bluetooth.BluetoothLeAudioContentMetadata;
|
||||
import android.bluetooth.BluetoothLeBroadcastMetadata;
|
||||
import android.bluetooth.BluetoothLeBroadcastReceiveState;
|
||||
import android.bluetooth.BluetoothProfile;
|
||||
import android.content.Context;
|
||||
import android.os.Looper;
|
||||
import android.platform.test.flag.junit.SetFlagsRule;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.TextView;
|
||||
@@ -96,6 +100,8 @@ import java.util.List;
|
||||
})
|
||||
public class AudioStreamsProgressCategoryControllerTest {
|
||||
@Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
|
||||
@Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
|
||||
|
||||
private static final String VALID_METADATA =
|
||||
"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=;;";
|
||||
@@ -115,6 +121,7 @@ public class AudioStreamsProgressCategoryControllerTest {
|
||||
@Mock private BluetoothLeBroadcastMetadata mMetadata;
|
||||
@Mock private CachedBluetoothDevice mDevice;
|
||||
@Mock private AudioStreamsProgressCategoryPreference mPreference;
|
||||
@Mock private BluetoothDevice mSourceDevice;
|
||||
private Lifecycle mLifecycle;
|
||||
private LifecycleOwner mLifecycleOwner;
|
||||
private Fragment mFragment;
|
||||
@@ -125,6 +132,7 @@ public class AudioStreamsProgressCategoryControllerTest {
|
||||
ShadowAudioStreamsHelper.setUseMock(mAudioStreamsHelper);
|
||||
when(mAudioStreamsHelper.getLeBroadcastAssistant()).thenReturn(mLeBroadcastAssistant);
|
||||
when(mAudioStreamsHelper.getAllConnectedSources()).thenReturn(emptyList());
|
||||
mSetFlagsRule.disableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
|
||||
|
||||
ShadowBluetoothUtils.sLocalBluetoothManager = mLocalBtManager;
|
||||
when(mLocalBtManager.getEventManager()).thenReturn(mBluetoothEventManager);
|
||||
@@ -282,6 +290,29 @@ public class AudioStreamsProgressCategoryControllerTest {
|
||||
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
|
||||
public void testOnStart_handleSourceFromQrCode() {
|
||||
// Setup a device
|
||||
@@ -764,6 +795,58 @@ public class AudioStreamsProgressCategoryControllerTest {
|
||||
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) {
|
||||
var connected = mock(BluetoothLeBroadcastReceiveState.class);
|
||||
List<Long> bisSyncState = new ArrayList<>();
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -59,6 +59,11 @@ public class ShadowAudioStreamsHelper {
|
||||
return sMockHelper.getAllConnectedSources();
|
||||
}
|
||||
|
||||
@Implementation
|
||||
public List<BluetoothLeBroadcastReceiveState> getAllPresentSources() {
|
||||
return sMockHelper.getAllPresentSources();
|
||||
}
|
||||
|
||||
/** Gets {@link CachedBluetoothDevice} in sharing or le connected */
|
||||
@Implementation
|
||||
public static Optional<CachedBluetoothDevice> getCachedBluetoothDeviceInSharingOrLeConnected(
|
||||
|
Reference in New Issue
Block a user