[Audiosharing] Handle sync, add source via qrcode.
Bug: 305620450
Test: manual
Change-Id: I32c14607035d8f37f44186175657c42307780e7b
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamPreference.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamPreference.java
index ffb0b88..678f952 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamPreference.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamPreference.java
@@ -35,21 +35,27 @@
*/
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 @@
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 @@
}
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 @@
.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;
+ }
+ }
}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsDashboardFragment.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsDashboardFragment.java
index a418415..b0af7dd 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsDashboardFragment.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsDashboardFragment.java
@@ -34,7 +34,7 @@
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 @@
@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 @@
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);
}
}
}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsHelper.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsHelper.java
index 198e8e5..2c6eedb 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsHelper.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsHelper.java
@@ -109,13 +109,14 @@
}
/** 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 @@
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;
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java
index 3c005b2..ab380c8 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java
@@ -25,6 +25,7 @@
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 @@
}
};
+ 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 @@
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 @@
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 @@
}
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 @@
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 @@
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 @@
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 @@
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 @@
.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;
+ }
+ }
}