[Audiosharing] Handle sync, add source via qrcode.

Bug: 305620450
Test: manual
Change-Id: I32c14607035d8f37f44186175657c42307780e7b
This commit is contained in:
chelseahao
2024-01-11 14:30:00 +08:00
committed by Chelsea Hao
parent f1c141906a
commit 5d0bd7555d
4 changed files with 308 additions and 52 deletions

View File

@@ -35,21 +35,27 @@ import com.google.common.base.Strings;
*/
class AudioStreamPreference extends TwoTargetPreference {
private boolean mIsConnected = false;
private AudioStream mAudioStream;
/**
* Update preference UI based on connection status
*
* @param isConnected Is this streams connected
* @param isConnected Is this stream connected
* @param summary Summary text
* @param onPreferenceClickListener Click listener for the preference
*/
void setIsConnected(
boolean isConnected, @Nullable OnPreferenceClickListener onPreferenceClickListener) {
boolean isConnected,
String summary,
@Nullable OnPreferenceClickListener onPreferenceClickListener) {
if (mIsConnected == isConnected
&& getSummary() == summary
&& getOnPreferenceClickListener() == onPreferenceClickListener) {
// Nothing to update.
return;
}
mIsConnected = isConnected;
setSummary(isConnected ? "Listening now" : "");
setSummary(summary);
setOrder(isConnected ? 0 : 1);
setOnPreferenceClickListener(onPreferenceClickListener);
notifyChanged();
@@ -60,6 +66,14 @@ class AudioStreamPreference extends TwoTargetPreference {
setIcon(R.drawable.ic_bt_audio_sharing);
}
void setAudioStreamState(AudioStreamsProgressCategoryController.AudioStreamState state) {
mAudioStream.setState(state);
}
AudioStreamsProgressCategoryController.AudioStreamState getAudioStreamState() {
return mAudioStream.getState();
}
@Override
protected boolean shouldHideSecondTarget() {
return mIsConnected;
@@ -71,19 +85,31 @@ class AudioStreamPreference extends TwoTargetPreference {
}
static AudioStreamPreference fromMetadata(
Context context, BluetoothLeBroadcastMetadata source) {
Context context,
BluetoothLeBroadcastMetadata source,
AudioStreamsProgressCategoryController.AudioStreamState streamState) {
AudioStreamPreference preference = new AudioStreamPreference(context, /* attrs= */ null);
preference.setTitle(getBroadcastName(source));
preference.setAudioStream(new AudioStream(source.getBroadcastId(), streamState));
return preference;
}
static AudioStreamPreference fromReceiveState(
Context context, BluetoothLeBroadcastReceiveState state) {
Context context,
BluetoothLeBroadcastReceiveState receiveState,
AudioStreamsProgressCategoryController.AudioStreamState streamState) {
AudioStreamPreference preference = new AudioStreamPreference(context, /* attrs= */ null);
preference.setTitle(getBroadcastName(state));
preference.setTitle(getBroadcastName(receiveState));
preference.setAudioStream(
new AudioStream(
receiveState.getSourceId(), receiveState.getBroadcastId(), streamState));
return preference;
}
private void setAudioStream(AudioStream audioStream) {
mAudioStream = audioStream;
}
private static String getBroadcastName(BluetoothLeBroadcastMetadata source) {
return source.getSubgroups().stream()
.map(s -> s.getContentMetadata().getProgramInfo())
@@ -99,4 +125,43 @@ class AudioStreamPreference extends TwoTargetPreference {
.findFirst()
.orElse("Broadcast Id: " + state.getBroadcastId());
}
private static final class AudioStream {
private int mSourceId;
private int mBroadcastId;
private AudioStreamsProgressCategoryController.AudioStreamState mState;
private AudioStream(
int broadcastId, AudioStreamsProgressCategoryController.AudioStreamState state) {
mBroadcastId = broadcastId;
mState = state;
}
private AudioStream(
int sourceId,
int broadcastId,
AudioStreamsProgressCategoryController.AudioStreamState state) {
mSourceId = sourceId;
mBroadcastId = broadcastId;
mState = state;
}
// TODO(chelseahao): use this to handleSourceRemoved
private int getSourceId() {
return mSourceId;
}
// TODO(chelseahao): use this to handleSourceRemoved
private int getBroadcastId() {
return mBroadcastId;
}
private AudioStreamsProgressCategoryController.AudioStreamState getState() {
return mState;
}
private void setState(AudioStreamsProgressCategoryController.AudioStreamState state) {
mState = state;
}
}
}

View File

@@ -34,7 +34,7 @@ import com.android.settingslib.bluetooth.BluetoothUtils;
public class AudioStreamsDashboardFragment extends DashboardFragment {
private static final String TAG = "AudioStreamsDashboardFrag";
private static final boolean DEBUG = BluetoothUtils.D;
private AudioStreamsScanQrCodeController mAudioStreamsScanQrCodeController;
private AudioStreamsProgressCategoryController mAudioStreamsProgressCategoryController;
public AudioStreamsDashboardFragment() {
super();
@@ -69,8 +69,8 @@ public class AudioStreamsDashboardFragment extends DashboardFragment {
@Override
public void onAttach(Context context) {
super.onAttach(context);
mAudioStreamsScanQrCodeController = use(AudioStreamsScanQrCodeController.class);
mAudioStreamsScanQrCodeController.setFragment(this);
use(AudioStreamsScanQrCodeController.class).setFragment(this);
mAudioStreamsProgressCategoryController = use(AudioStreamsProgressCategoryController.class);
}
@Override
@@ -103,11 +103,13 @@ public class AudioStreamsDashboardFragment extends DashboardFragment {
if (DEBUG) {
Log.d(TAG, "onActivityResult() broadcastId : " + source.getBroadcastId());
}
if (mAudioStreamsScanQrCodeController == null) {
Log.w(TAG, "onActivityResult() AudioStreamsScanQrCodeController is null!");
if (mAudioStreamsProgressCategoryController == null) {
Log.w(
TAG,
"onActivityResult() AudioStreamsProgressCategoryController is null!");
return;
}
mAudioStreamsScanQrCodeController.addSource(source);
mAudioStreamsProgressCategoryController.setSourceFromQrCode(source);
}
}
}

View File

@@ -109,13 +109,14 @@ class AudioStreamsHelper {
}
/** Retrieves a list of all LE broadcast receive states from active sinks. */
List<BluetoothLeBroadcastReceiveState> getAllSources() {
List<BluetoothLeBroadcastReceiveState> getAllConnectedSources() {
if (mLeBroadcastAssistant == null) {
Log.w(TAG, "getAllSources(): LeBroadcastAssistant is null!");
return emptyList();
}
return getActiveSinksOnAssistant(mBluetoothManager).stream()
.flatMap(sink -> mLeBroadcastAssistant.getAllSources(sink).stream())
.filter(this::isConnected)
.toList();
}
@@ -124,7 +125,7 @@ class AudioStreamsHelper {
return mLeBroadcastAssistant;
}
static boolean isConnected(BluetoothLeBroadcastReceiveState state) {
boolean isConnected(BluetoothLeBroadcastReceiveState state) {
return state.getPaSyncState() == BluetoothLeBroadcastReceiveState.PA_SYNC_STATE_SYNCHRONIZED
&& state.getBigEncryptionState()
== BluetoothLeBroadcastReceiveState.BIG_ENCRYPTION_STATE_DECRYPTING;

View File

@@ -25,6 +25,7 @@ import android.bluetooth.BluetoothLeBroadcastReceiveState;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.os.Bundle;
import android.os.CountDownTimer;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
@@ -71,6 +72,17 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
}
};
enum AudioStreamState {
// When mTimedSourceFromQrCode is present and this source has not been synced.
WAIT_FOR_SYNC,
// When source has been synced but not added to any sink.
SYNCED,
// When addSource is called for this source and waiting for response.
WAIT_FOR_SOURCE_ADD,
// Source is added to active sink.
SOURCE_ADDED,
}
private final Executor mExecutor;
private final AudioStreamsBroadcastAssistantCallback mBroadcastAssistantCallback;
private final AudioStreamsHelper mAudioStreamsHelper;
@@ -78,6 +90,7 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
private final @Nullable LocalBluetoothManager mBluetoothManager;
private final ConcurrentHashMap<Integer, AudioStreamPreference> mBroadcastIdToPreferenceMap =
new ConcurrentHashMap<>();
private TimedSourceFromQrCode mTimedSourceFromQrCode;
private AudioStreamsProgressCategoryPreference mCategoryPreference;
public AudioStreamsProgressCategoryController(Context context, String preferenceKey) {
@@ -122,6 +135,12 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
mExecutor.execute(this::stopScanning);
}
void setSourceFromQrCode(BluetoothLeBroadcastMetadata source) {
mTimedSourceFromQrCode =
new TimedSourceFromQrCode(
mContext, source, () -> handleSourceLost(source.getBroadcastId()));
}
void setScanning(boolean isScanning) {
ThreadUtils.postOnMainThread(
() -> {
@@ -140,24 +159,90 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
}
if (source.isEncrypted()) {
ThreadUtils.postOnMainThread(
() -> launchPasswordDialog(source, preference));
() ->
launchPasswordDialog(
source, (AudioStreamPreference) preference));
} else {
mAudioStreamsHelper.addSource(source);
((AudioStreamPreference) preference)
.setAudioStreamState(AudioStreamState.WAIT_FOR_SOURCE_ADD);
updatePreferenceConnectionState(
(AudioStreamPreference) preference,
AudioStreamState.WAIT_FOR_SOURCE_ADD,
null);
}
return true;
};
mBroadcastIdToPreferenceMap.computeIfAbsent(
source.getBroadcastId(),
k -> {
var preference = AudioStreamPreference.fromMetadata(mContext, source);
ThreadUtils.postOnMainThread(
() -> {
preference.setIsConnected(false, addSourceOrShowDialog);
if (mCategoryPreference != null) {
mCategoryPreference.addPreference(preference);
}
});
return preference;
var broadcastIdFound = source.getBroadcastId();
mBroadcastIdToPreferenceMap.compute(
broadcastIdFound,
(k, v) -> {
if (v == null) {
return addNewPreference(
source, AudioStreamState.SYNCED, addSourceOrShowDialog);
}
var fromState = v.getAudioStreamState();
if (fromState == AudioStreamState.WAIT_FOR_SYNC) {
var pendingSource = mTimedSourceFromQrCode.get();
if (pendingSource == null) {
Log.w(
TAG,
"handleSourceFound(): unexpected state with null pendingSource:"
+ fromState
+ " for broadcastId : "
+ broadcastIdFound);
v.setAudioStreamState(AudioStreamState.SYNCED);
return v;
}
mAudioStreamsHelper.addSource(pendingSource);
mTimedSourceFromQrCode.consumed();
v.setAudioStreamState(AudioStreamState.WAIT_FOR_SOURCE_ADD);
updatePreferenceConnectionState(
v, AudioStreamState.WAIT_FOR_SOURCE_ADD, null);
} else {
if (fromState != AudioStreamState.SOURCE_ADDED) {
Log.w(
TAG,
"handleSourceFound(): unexpected state : "
+ fromState
+ " for broadcastId : "
+ broadcastIdFound);
}
}
return v;
});
}
private void handleSourceFromQrCodeIfExists() {
if (mTimedSourceFromQrCode == null || mTimedSourceFromQrCode.get() == null) {
return;
}
var metadataFromQrCode = mTimedSourceFromQrCode.get();
mBroadcastIdToPreferenceMap.compute(
metadataFromQrCode.getBroadcastId(),
(k, v) -> {
if (v == null) {
mTimedSourceFromQrCode.waitForConsume();
return addNewPreference(
metadataFromQrCode, AudioStreamState.WAIT_FOR_SYNC, null);
}
var fromState = v.getAudioStreamState();
if (fromState == AudioStreamState.SYNCED) {
mAudioStreamsHelper.addSource(metadataFromQrCode);
mTimedSourceFromQrCode.consumed();
v.setAudioStreamState(AudioStreamState.WAIT_FOR_SOURCE_ADD);
updatePreferenceConnectionState(
v, AudioStreamState.WAIT_FOR_SOURCE_ADD, null);
} else {
Log.w(
TAG,
"handleSourceFromQrCode(): unexpected state : "
+ fromState
+ " for broadcastId : "
+ metadataFromQrCode.getBroadcastId());
}
return v;
});
}
@@ -174,32 +259,54 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
mAudioStreamsHelper.removeSource(broadcastId);
}
void handleSourceConnected(BluetoothLeBroadcastReceiveState state) {
if (!AudioStreamsHelper.isConnected(state)) {
void handleSourceConnected(BluetoothLeBroadcastReceiveState receiveState) {
if (!mAudioStreamsHelper.isConnected(receiveState)) {
return;
}
var sourceAddedState = AudioStreamState.SOURCE_ADDED;
var broadcastIdConnected = receiveState.getBroadcastId();
mBroadcastIdToPreferenceMap.compute(
state.getBroadcastId(),
broadcastIdConnected,
(k, v) -> {
// True if this source has been added either by scanning, or it's currently
// connected to another active sink.
boolean existed = v != null;
AudioStreamPreference preference =
existed ? v : AudioStreamPreference.fromReceiveState(mContext, state);
ThreadUtils.postOnMainThread(
() -> {
preference.setIsConnected(
true, p -> launchDetailFragment(state.getBroadcastId()));
if (mCategoryPreference != null && !existed) {
mCategoryPreference.addPreference(preference);
}
});
return preference;
if (v == null) {
return addNewPreference(
receiveState,
sourceAddedState,
p -> launchDetailFragment(broadcastIdConnected));
}
var fromState = v.getAudioStreamState();
if (fromState == AudioStreamState.WAIT_FOR_SOURCE_ADD
|| fromState == AudioStreamState.SYNCED
|| fromState == AudioStreamState.WAIT_FOR_SYNC) {
if (mTimedSourceFromQrCode != null) {
mTimedSourceFromQrCode.consumed();
}
} else {
if (fromState != AudioStreamState.SOURCE_ADDED) {
Log.w(
TAG,
"handleSourceConnected(): unexpected state : "
+ fromState
+ " for broadcastId : "
+ broadcastIdConnected);
}
}
v.setAudioStreamState(sourceAddedState);
updatePreferenceConnectionState(
v, sourceAddedState, p -> launchDetailFragment(broadcastIdConnected));
return v;
});
}
private static String getPreferenceSummary(AudioStreamState state) {
return switch (state) {
case WAIT_FOR_SYNC -> "Scanning...";
case WAIT_FOR_SOURCE_ADD -> "Connecting...";
case SOURCE_ADDED -> "Listening now";
default -> "";
};
}
void showToast(String msg) {
AudioSharingUtils.toastMessage(mContext, msg);
}
@@ -235,13 +342,15 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
mLeBroadcastAssistant.registerServiceCallBack(mExecutor, mBroadcastAssistantCallback);
mLeBroadcastAssistant.startSearchingForSources(emptyList());
// Display currently connected streams
// Handle QR code scan and display currently connected streams
var unused =
ThreadUtils.postOnBackgroundThread(
() ->
mAudioStreamsHelper
.getAllSources()
.forEach(this::handleSourceConnected));
() -> {
handleSourceFromQrCodeIfExists();
mAudioStreamsHelper
.getAllConnectedSources()
.forEach(this::handleSourceConnected);
});
}
private void stopScanning() {
@@ -256,6 +365,43 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
mLeBroadcastAssistant.stopSearchingForSources();
}
mLeBroadcastAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback);
if (mTimedSourceFromQrCode != null) {
mTimedSourceFromQrCode.consumed();
}
}
private AudioStreamPreference addNewPreference(
BluetoothLeBroadcastReceiveState receiveState,
AudioStreamState state,
Preference.OnPreferenceClickListener onClickListener) {
var preference = AudioStreamPreference.fromReceiveState(mContext, receiveState, state);
updatePreferenceConnectionState(preference, state, onClickListener);
return preference;
}
private AudioStreamPreference addNewPreference(
BluetoothLeBroadcastMetadata metadata,
AudioStreamState state,
Preference.OnPreferenceClickListener onClickListener) {
var preference = AudioStreamPreference.fromMetadata(mContext, metadata, state);
updatePreferenceConnectionState(preference, state, onClickListener);
return preference;
}
private void updatePreferenceConnectionState(
AudioStreamPreference preference,
AudioStreamState state,
Preference.OnPreferenceClickListener onClickListener) {
ThreadUtils.postOnMainThread(
() -> {
preference.setIsConnected(
state == AudioStreamState.SOURCE_ADDED,
getPreferenceSummary(state),
onClickListener);
if (mCategoryPreference != null) {
mCategoryPreference.addPreference(preference);
}
});
}
private boolean launchDetailFragment(int broadcastId) {
@@ -282,7 +428,8 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
return true;
}
private void launchPasswordDialog(BluetoothLeBroadcastMetadata source, Preference preference) {
private void launchPasswordDialog(
BluetoothLeBroadcastMetadata source, AudioStreamPreference preference) {
View layout =
LayoutInflater.from(mContext)
.inflate(R.layout.bluetooth_find_broadcast_password_dialog, null);
@@ -307,8 +454,49 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
.setBroadcastCode(
code.getBytes(StandardCharsets.UTF_8))
.build());
preference.setAudioStreamState(
AudioStreamState.WAIT_FOR_SOURCE_ADD);
updatePreferenceConnectionState(
preference, AudioStreamState.WAIT_FOR_SOURCE_ADD, null);
})
.create();
alertDialog.show();
}
private static class TimedSourceFromQrCode {
private static final int WAIT_FOR_SYNC_TIMEOUT_MILLIS = 15000;
private final CountDownTimer mTimer;
private BluetoothLeBroadcastMetadata mSourceFromQrCode;
private TimedSourceFromQrCode(
Context context,
BluetoothLeBroadcastMetadata sourceFromQrCode,
Runnable timeoutAction) {
mSourceFromQrCode = sourceFromQrCode;
mTimer =
new CountDownTimer(WAIT_FOR_SYNC_TIMEOUT_MILLIS, 1000) {
@Override
public void onTick(long millisUntilFinished) {}
@Override
public void onFinish() {
timeoutAction.run();
AudioSharingUtils.toastMessage(context, "Audio steam isn't available");
}
};
}
private void waitForConsume() {
mTimer.start();
}
private void consumed() {
mTimer.cancel();
mSourceFromQrCode = null;
}
private BluetoothLeBroadcastMetadata get() {
return mSourceFromQrCode;
}
}
}