Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 5d0bd755 authored by chelseahao's avatar chelseahao Committed by Chelsea Hao
Browse files

[Audiosharing] Handle sync, add source via qrcode.

Bug: 305620450
Test: manual
Change-Id: I32c14607035d8f37f44186175657c42307780e7b
parent f1c14190
Loading
Loading
Loading
Loading
+71 −6
Original line number Diff line number Diff line
@@ -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;
        }
    }
}
+8 −6
Original line number Diff line number Diff line
@@ -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);
            }
        }
    }
+3 −2
Original line number Diff line number Diff line
@@ -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;
+226 −38
Original line number Diff line number Diff line
@@ -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);

        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;
                });
    }

@@ -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) {
@@ -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() {
@@ -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;
        }
    }
}