[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 { class AudioStreamPreference extends TwoTargetPreference {
private boolean mIsConnected = false; private boolean mIsConnected = false;
private AudioStream mAudioStream;
/** /**
* Update preference UI based on connection status * 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( void setIsConnected(
boolean isConnected, @Nullable OnPreferenceClickListener onPreferenceClickListener) { boolean isConnected,
String summary,
@Nullable OnPreferenceClickListener onPreferenceClickListener) {
if (mIsConnected == isConnected if (mIsConnected == isConnected
&& getSummary() == summary
&& getOnPreferenceClickListener() == onPreferenceClickListener) { && getOnPreferenceClickListener() == onPreferenceClickListener) {
// Nothing to update. // Nothing to update.
return; return;
} }
mIsConnected = isConnected; mIsConnected = isConnected;
setSummary(isConnected ? "Listening now" : ""); setSummary(summary);
setOrder(isConnected ? 0 : 1); setOrder(isConnected ? 0 : 1);
setOnPreferenceClickListener(onPreferenceClickListener); setOnPreferenceClickListener(onPreferenceClickListener);
notifyChanged(); notifyChanged();
@@ -60,6 +66,14 @@ class AudioStreamPreference extends TwoTargetPreference {
setIcon(R.drawable.ic_bt_audio_sharing); setIcon(R.drawable.ic_bt_audio_sharing);
} }
void setAudioStreamState(AudioStreamsProgressCategoryController.AudioStreamState state) {
mAudioStream.setState(state);
}
AudioStreamsProgressCategoryController.AudioStreamState getAudioStreamState() {
return mAudioStream.getState();
}
@Override @Override
protected boolean shouldHideSecondTarget() { protected boolean shouldHideSecondTarget() {
return mIsConnected; return mIsConnected;
@@ -71,19 +85,31 @@ class AudioStreamPreference extends TwoTargetPreference {
} }
static AudioStreamPreference fromMetadata( static AudioStreamPreference fromMetadata(
Context context, BluetoothLeBroadcastMetadata source) { Context context,
BluetoothLeBroadcastMetadata source,
AudioStreamsProgressCategoryController.AudioStreamState streamState) {
AudioStreamPreference preference = new AudioStreamPreference(context, /* attrs= */ null); AudioStreamPreference preference = new AudioStreamPreference(context, /* attrs= */ null);
preference.setTitle(getBroadcastName(source)); preference.setTitle(getBroadcastName(source));
preference.setAudioStream(new AudioStream(source.getBroadcastId(), streamState));
return preference; return preference;
} }
static AudioStreamPreference fromReceiveState( static AudioStreamPreference fromReceiveState(
Context context, BluetoothLeBroadcastReceiveState state) { Context context,
BluetoothLeBroadcastReceiveState receiveState,
AudioStreamsProgressCategoryController.AudioStreamState streamState) {
AudioStreamPreference preference = new AudioStreamPreference(context, /* attrs= */ null); 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; return preference;
} }
private void setAudioStream(AudioStream audioStream) {
mAudioStream = audioStream;
}
private static String getBroadcastName(BluetoothLeBroadcastMetadata source) { private static String getBroadcastName(BluetoothLeBroadcastMetadata source) {
return source.getSubgroups().stream() return source.getSubgroups().stream()
.map(s -> s.getContentMetadata().getProgramInfo()) .map(s -> s.getContentMetadata().getProgramInfo())
@@ -99,4 +125,43 @@ class AudioStreamPreference extends TwoTargetPreference {
.findFirst() .findFirst()
.orElse("Broadcast Id: " + state.getBroadcastId()); .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 { public class AudioStreamsDashboardFragment extends DashboardFragment {
private static final String TAG = "AudioStreamsDashboardFrag"; private static final String TAG = "AudioStreamsDashboardFrag";
private static final boolean DEBUG = BluetoothUtils.D; private static final boolean DEBUG = BluetoothUtils.D;
private AudioStreamsScanQrCodeController mAudioStreamsScanQrCodeController; private AudioStreamsProgressCategoryController mAudioStreamsProgressCategoryController;
public AudioStreamsDashboardFragment() { public AudioStreamsDashboardFragment() {
super(); super();
@@ -69,8 +69,8 @@ public class AudioStreamsDashboardFragment extends DashboardFragment {
@Override @Override
public void onAttach(Context context) { public void onAttach(Context context) {
super.onAttach(context); super.onAttach(context);
mAudioStreamsScanQrCodeController = use(AudioStreamsScanQrCodeController.class); use(AudioStreamsScanQrCodeController.class).setFragment(this);
mAudioStreamsScanQrCodeController.setFragment(this); mAudioStreamsProgressCategoryController = use(AudioStreamsProgressCategoryController.class);
} }
@Override @Override
@@ -103,11 +103,13 @@ public class AudioStreamsDashboardFragment extends DashboardFragment {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "onActivityResult() broadcastId : " + source.getBroadcastId()); Log.d(TAG, "onActivityResult() broadcastId : " + source.getBroadcastId());
} }
if (mAudioStreamsScanQrCodeController == null) { if (mAudioStreamsProgressCategoryController == null) {
Log.w(TAG, "onActivityResult() AudioStreamsScanQrCodeController is null!"); Log.w(
TAG,
"onActivityResult() AudioStreamsProgressCategoryController is null!");
return; 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. */ /** Retrieves a list of all LE broadcast receive states from active sinks. */
List<BluetoothLeBroadcastReceiveState> getAllSources() { List<BluetoothLeBroadcastReceiveState> getAllConnectedSources() {
if (mLeBroadcastAssistant == null) { if (mLeBroadcastAssistant == null) {
Log.w(TAG, "getAllSources(): LeBroadcastAssistant is null!"); Log.w(TAG, "getAllSources(): LeBroadcastAssistant is null!");
return emptyList(); return emptyList();
} }
return getActiveSinksOnAssistant(mBluetoothManager).stream() return getActiveSinksOnAssistant(mBluetoothManager).stream()
.flatMap(sink -> mLeBroadcastAssistant.getAllSources(sink).stream()) .flatMap(sink -> mLeBroadcastAssistant.getAllSources(sink).stream())
.filter(this::isConnected)
.toList(); .toList();
} }
@@ -124,7 +125,7 @@ class AudioStreamsHelper {
return mLeBroadcastAssistant; return mLeBroadcastAssistant;
} }
static boolean isConnected(BluetoothLeBroadcastReceiveState state) { boolean isConnected(BluetoothLeBroadcastReceiveState state) {
return state.getPaSyncState() == BluetoothLeBroadcastReceiveState.PA_SYNC_STATE_SYNCHRONIZED return state.getPaSyncState() == BluetoothLeBroadcastReceiveState.PA_SYNC_STATE_SYNCHRONIZED
&& state.getBigEncryptionState() && state.getBigEncryptionState()
== BluetoothLeBroadcastReceiveState.BIG_ENCRYPTION_STATE_DECRYPTING; == BluetoothLeBroadcastReceiveState.BIG_ENCRYPTION_STATE_DECRYPTING;

View File

@@ -25,6 +25,7 @@ import android.bluetooth.BluetoothLeBroadcastReceiveState;
import android.bluetooth.BluetoothProfile; import android.bluetooth.BluetoothProfile;
import android.content.Context; import android.content.Context;
import android.os.Bundle; import android.os.Bundle;
import android.os.CountDownTimer;
import android.util.Log; import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; 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 Executor mExecutor;
private final AudioStreamsBroadcastAssistantCallback mBroadcastAssistantCallback; private final AudioStreamsBroadcastAssistantCallback mBroadcastAssistantCallback;
private final AudioStreamsHelper mAudioStreamsHelper; private final AudioStreamsHelper mAudioStreamsHelper;
@@ -78,6 +90,7 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
private final @Nullable LocalBluetoothManager mBluetoothManager; private final @Nullable LocalBluetoothManager mBluetoothManager;
private final ConcurrentHashMap<Integer, AudioStreamPreference> mBroadcastIdToPreferenceMap = private final ConcurrentHashMap<Integer, AudioStreamPreference> mBroadcastIdToPreferenceMap =
new ConcurrentHashMap<>(); new ConcurrentHashMap<>();
private TimedSourceFromQrCode mTimedSourceFromQrCode;
private AudioStreamsProgressCategoryPreference mCategoryPreference; private AudioStreamsProgressCategoryPreference mCategoryPreference;
public AudioStreamsProgressCategoryController(Context context, String preferenceKey) { public AudioStreamsProgressCategoryController(Context context, String preferenceKey) {
@@ -122,6 +135,12 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
mExecutor.execute(this::stopScanning); mExecutor.execute(this::stopScanning);
} }
void setSourceFromQrCode(BluetoothLeBroadcastMetadata source) {
mTimedSourceFromQrCode =
new TimedSourceFromQrCode(
mContext, source, () -> handleSourceLost(source.getBroadcastId()));
}
void setScanning(boolean isScanning) { void setScanning(boolean isScanning) {
ThreadUtils.postOnMainThread( ThreadUtils.postOnMainThread(
() -> { () -> {
@@ -140,24 +159,90 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
} }
if (source.isEncrypted()) { if (source.isEncrypted()) {
ThreadUtils.postOnMainThread( ThreadUtils.postOnMainThread(
() -> launchPasswordDialog(source, preference)); () ->
launchPasswordDialog(
source, (AudioStreamPreference) preference));
} else { } else {
mAudioStreamsHelper.addSource(source); mAudioStreamsHelper.addSource(source);
((AudioStreamPreference) preference)
.setAudioStreamState(AudioStreamState.WAIT_FOR_SOURCE_ADD);
updatePreferenceConnectionState(
(AudioStreamPreference) preference,
AudioStreamState.WAIT_FOR_SOURCE_ADD,
null);
} }
return true; return true;
}; };
mBroadcastIdToPreferenceMap.computeIfAbsent(
source.getBroadcastId(), var broadcastIdFound = source.getBroadcastId();
k -> { mBroadcastIdToPreferenceMap.compute(
var preference = AudioStreamPreference.fromMetadata(mContext, source); broadcastIdFound,
ThreadUtils.postOnMainThread( (k, v) -> {
() -> { if (v == null) {
preference.setIsConnected(false, addSourceOrShowDialog); return addNewPreference(
if (mCategoryPreference != null) { source, AudioStreamState.SYNCED, addSourceOrShowDialog);
mCategoryPreference.addPreference(preference);
} }
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;
}); });
return preference; }
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,30 +259,52 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
mAudioStreamsHelper.removeSource(broadcastId); mAudioStreamsHelper.removeSource(broadcastId);
} }
void handleSourceConnected(BluetoothLeBroadcastReceiveState state) { void handleSourceConnected(BluetoothLeBroadcastReceiveState receiveState) {
if (!AudioStreamsHelper.isConnected(state)) { if (!mAudioStreamsHelper.isConnected(receiveState)) {
return; return;
} }
var sourceAddedState = AudioStreamState.SOURCE_ADDED;
var broadcastIdConnected = receiveState.getBroadcastId();
mBroadcastIdToPreferenceMap.compute( mBroadcastIdToPreferenceMap.compute(
state.getBroadcastId(), broadcastIdConnected,
(k, v) -> { (k, v) -> {
// True if this source has been added either by scanning, or it's currently if (v == null) {
// connected to another active sink. return addNewPreference(
boolean existed = v != null; receiveState,
AudioStreamPreference preference = sourceAddedState,
existed ? v : AudioStreamPreference.fromReceiveState(mContext, state); p -> launchDetailFragment(broadcastIdConnected));
ThreadUtils.postOnMainThread(
() -> {
preference.setIsConnected(
true, p -> launchDetailFragment(state.getBroadcastId()));
if (mCategoryPreference != null && !existed) {
mCategoryPreference.addPreference(preference);
} }
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;
}); });
}
return preference; 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) { void showToast(String msg) {
@@ -235,13 +342,15 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
mLeBroadcastAssistant.registerServiceCallBack(mExecutor, mBroadcastAssistantCallback); mLeBroadcastAssistant.registerServiceCallBack(mExecutor, mBroadcastAssistantCallback);
mLeBroadcastAssistant.startSearchingForSources(emptyList()); mLeBroadcastAssistant.startSearchingForSources(emptyList());
// Display currently connected streams // Handle QR code scan and display currently connected streams
var unused = var unused =
ThreadUtils.postOnBackgroundThread( ThreadUtils.postOnBackgroundThread(
() -> () -> {
handleSourceFromQrCodeIfExists();
mAudioStreamsHelper mAudioStreamsHelper
.getAllSources() .getAllConnectedSources()
.forEach(this::handleSourceConnected)); .forEach(this::handleSourceConnected);
});
} }
private void stopScanning() { private void stopScanning() {
@@ -256,6 +365,43 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
mLeBroadcastAssistant.stopSearchingForSources(); mLeBroadcastAssistant.stopSearchingForSources();
} }
mLeBroadcastAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback); 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) { private boolean launchDetailFragment(int broadcastId) {
@@ -282,7 +428,8 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
return true; return true;
} }
private void launchPasswordDialog(BluetoothLeBroadcastMetadata source, Preference preference) { private void launchPasswordDialog(
BluetoothLeBroadcastMetadata source, AudioStreamPreference preference) {
View layout = View layout =
LayoutInflater.from(mContext) LayoutInflater.from(mContext)
.inflate(R.layout.bluetooth_find_broadcast_password_dialog, null); .inflate(R.layout.bluetooth_find_broadcast_password_dialog, null);
@@ -307,8 +454,49 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
.setBroadcastCode( .setBroadcastCode(
code.getBytes(StandardCharsets.UTF_8)) code.getBytes(StandardCharsets.UTF_8))
.build()); .build());
preference.setAudioStreamState(
AudioStreamState.WAIT_FOR_SOURCE_ADD);
updatePreferenceConnectionState(
preference, AudioStreamState.WAIT_FOR_SOURCE_ADD, null);
}) })
.create(); .create();
alertDialog.show(); 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;
}
}
} }