Loading src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamPreference.java +71 −6 Original line number Diff line number Diff line Loading @@ -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(); Loading @@ -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; Loading @@ -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()) Loading @@ -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; } } } src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsDashboardFragment.java +8 −6 Original line number Diff line number Diff line Loading @@ -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(); Loading Loading @@ -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 Loading Loading @@ -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); } } } Loading src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsHelper.java +3 −2 Original line number Diff line number Diff line Loading @@ -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(); } Loading @@ -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; Loading src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java +226 −38 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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; Loading @@ -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) { Loading Loading @@ -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( () -> { Loading @@ -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); 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; }); 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; }); } Loading @@ -174,30 +259,52 @@ 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); 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; }); } 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) { Loading Loading @@ -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( () -> () -> { handleSourceFromQrCodeIfExists(); mAudioStreamsHelper .getAllSources() .forEach(this::handleSourceConnected)); .getAllConnectedSources() .forEach(this::handleSourceConnected); }); } private void stopScanning() { Loading @@ -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) { Loading @@ -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); Loading @@ -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; } } } Loading
src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamPreference.java +71 −6 Original line number Diff line number Diff line Loading @@ -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(); Loading @@ -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; Loading @@ -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()) Loading @@ -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; } } }
src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsDashboardFragment.java +8 −6 Original line number Diff line number Diff line Loading @@ -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(); Loading Loading @@ -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 Loading Loading @@ -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); } } } Loading
src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsHelper.java +3 −2 Original line number Diff line number Diff line Loading @@ -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(); } Loading @@ -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; Loading
src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java +226 −38 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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; Loading @@ -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) { Loading Loading @@ -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( () -> { Loading @@ -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); 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; }); 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; }); } Loading @@ -174,30 +259,52 @@ 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); 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; }); } 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) { Loading Loading @@ -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( () -> () -> { handleSourceFromQrCodeIfExists(); mAudioStreamsHelper .getAllSources() .forEach(this::handleSourceConnected)); .getAllConnectedSources() .forEach(this::handleSourceConnected); }); } private void stopScanning() { Loading @@ -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) { Loading @@ -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); Loading @@ -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; } } }