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

Commit 1a7a4d24 authored by Chelsea Hao's avatar Chelsea Hao Committed by Android (Google) Code Review
Browse files

Merge "[Audiosharing] Start / stop broadcast scanning." into main

parents 7e346919 fdf67f0c
Loading
Loading
Loading
Loading
+2 −1
Original line number Diff line number Diff line
@@ -34,6 +34,7 @@

    <com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsProgressCategoryPreference
        android:key="audio_streams_nearby_category"
        android:title="@string/audio_streams_pref_title" />
        android:title="@string/audio_streams_pref_title"
        settings:controller="com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsProgressCategoryController" />

</PreferenceScreen>
 No newline at end of file
+19 −7
Original line number Diff line number Diff line
@@ -19,6 +19,8 @@ package com.android.settings.connecteddevice.audiosharing.audiostreams;
import android.content.Context;
import android.util.AttributeSet;

import androidx.annotation.Nullable;

import com.android.settings.R;
import com.android.settingslib.widget.TwoTargetPreference;

@@ -27,25 +29,35 @@ import com.android.settingslib.widget.TwoTargetPreference;
 * {@link TwoTargetPreference}.
 */
public class AudioStreamPreference extends TwoTargetPreference {
    private boolean mShowLock = true;
    private boolean mIsConnected = false;

    /**
     * Sets whether to display the lock icon.
     * Update preference UI based on connection status
     *
     * @param showLock Should show / hide the lock icon
     * @param isConnected Is this streams connected
     */
    public void setShowLock(boolean showLock) {
        mShowLock = showLock;
    public void setIsConnected(
            boolean isConnected, @Nullable OnPreferenceClickListener onPreferenceClickListener) {
        if (mIsConnected == isConnected
                && getOnPreferenceClickListener() == onPreferenceClickListener) {
            // Nothing to update.
            return;
        }
        mIsConnected = isConnected;
        setSummary(isConnected ? "Listening now" : "");
        setOrder(isConnected ? 0 : 1);
        setOnPreferenceClickListener(onPreferenceClickListener);
        notifyChanged();
    }

    public AudioStreamPreference(Context context, AttributeSet attrs) {
    public AudioStreamPreference(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        setIcon(R.drawable.ic_bt_audio_sharing);
    }

    @Override
    protected boolean shouldHideSecondTarget() {
        return !mShowLock;
        return mIsConnected;
    }

    @Override
+144 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.settings.connecteddevice.audiosharing.audiostreams;

import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothLeBroadcastAssistant;
import android.bluetooth.BluetoothLeBroadcastMetadata;
import android.bluetooth.BluetoothLeBroadcastReceiveState;
import android.util.Log;

import com.android.settingslib.bluetooth.BluetoothUtils;

import java.util.Locale;

public class AudioStreamsBroadcastAssistantCallback
        implements BluetoothLeBroadcastAssistant.Callback {

    private static final String TAG = "AudioStreamsBroadcastAssistantCallback";
    private static final boolean DEBUG = BluetoothUtils.D;

    private AudioStreamsProgressCategoryController mCategoryController;

    public AudioStreamsBroadcastAssistantCallback(
            AudioStreamsProgressCategoryController audioStreamsProgressCategoryController) {
        mCategoryController = audioStreamsProgressCategoryController;
    }

    @Override
    public void onReceiveStateChanged(
            BluetoothDevice sink, int sourceId, BluetoothLeBroadcastReceiveState state) {
        if (DEBUG) {
            Log.d(
                    TAG,
                    "onReceiveStateChanged() sink : "
                            + sink.getAddress()
                            + " sourceId: "
                            + sourceId
                            + " state: "
                            + state);
        }
    }

    @Override
    public void onSearchStartFailed(int reason) {
        Log.w(TAG, "onSearchStartFailed() reason : " + reason);
        mCategoryController.showToast(
                String.format(Locale.US, "Failed to start scanning, reason %d", reason));
    }

    @Override
    public void onSearchStarted(int reason) {
        if (mCategoryController == null) {
            Log.w(TAG, "onSearchStarted() : mCategoryController is null!");
            return;
        }
        if (DEBUG) {
            Log.d(TAG, "onSearchStarted() reason : " + reason);
        }
        mCategoryController.setScanning(true);
    }

    @Override
    public void onSearchStopFailed(int reason) {
        Log.w(TAG, "onSearchStopFailed() reason : " + reason);
        mCategoryController.showToast(
                String.format(Locale.US, "Failed to stop scanning, reason %d", reason));
    }

    @Override
    public void onSearchStopped(int reason) {
        if (mCategoryController == null) {
            Log.w(TAG, "onSearchStopped() : mCategoryController is null!");
            return;
        }
        if (DEBUG) {
            Log.d(TAG, "onSearchStopped() reason : " + reason);
        }
        mCategoryController.setScanning(false);
    }

    @Override
    public void onSourceAddFailed(
            BluetoothDevice sink, BluetoothLeBroadcastMetadata source, int reason) {}

    @Override
    public void onSourceAdded(BluetoothDevice sink, int sourceId, int reason) {
        if (DEBUG) {
            Log.d(
                    TAG,
                    "onSourceAdded() sink : "
                            + sink.getAddress()
                            + " sourceId: "
                            + sourceId
                            + " reason: "
                            + reason);
        }
    }

    @Override
    public void onSourceFound(BluetoothLeBroadcastMetadata source) {
        if (mCategoryController == null) {
            Log.w(TAG, "onSourceFound() : mCategoryController is null!");
            return;
        }
        if (DEBUG) {
            Log.d(TAG, "onSourceFound() broadcastId : " + source.getBroadcastId());
        }
        mCategoryController.addSourceFound(source);
    }

    @Override
    public void onSourceLost(int broadcastId) {
        if (DEBUG) {
            Log.d(TAG, "onSourceLost() broadcastId : " + broadcastId);
        }
        mCategoryController.removeSourceLost(broadcastId);
    }

    @Override
    public void onSourceModified(BluetoothDevice sink, int sourceId, int reason) {}

    @Override
    public void onSourceModifyFailed(BluetoothDevice sink, int sourceId, int reason) {}

    @Override
    public void onSourceRemoveFailed(BluetoothDevice sink, int sourceId, int reason) {}

    @Override
    public void onSourceRemoved(BluetoothDevice sink, int sourceId, int reason) {}
}
+265 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.settings.connecteddevice.audiosharing.audiostreams;

import static java.util.Collections.emptyList;

import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothLeAudioContentMetadata;
import android.bluetooth.BluetoothLeBroadcastMetadata;
import android.bluetooth.BluetoothLeBroadcastReceiveState;
import android.content.Context;
import android.util.Log;

import androidx.annotation.NonNull;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
import androidx.preference.Preference;
import androidx.preference.PreferenceScreen;

import com.android.settings.bluetooth.Utils;
import com.android.settings.connecteddevice.audiosharing.AudioSharingUtils;
import com.android.settings.core.BasePreferenceController;
import com.android.settingslib.bluetooth.BluetoothUtils;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
import com.android.settingslib.utils.ThreadUtils;

import com.google.common.base.Strings;

import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.stream.Stream;

import javax.annotation.Nullable;

public class AudioStreamsProgressCategoryController extends BasePreferenceController
        implements DefaultLifecycleObserver {
    private static final String TAG = "AudioStreamsProgressCategoryController";
    private static final boolean DEBUG = BluetoothUtils.D;

    private final Executor mExecutor;
    private final AudioStreamsBroadcastAssistantCallback mBroadcastAssistantCallback;
    private final LocalBluetoothManager mBluetoothManager;
    private final @Nullable LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant;
    private final ConcurrentHashMap<Integer, AudioStreamPreference> mBroadcastIdToPreferenceMap =
            new ConcurrentHashMap<>();
    private @Nullable AudioStreamsProgressCategoryPreference mCategoryPreference;

    public AudioStreamsProgressCategoryController(Context context, String preferenceKey) {
        super(context, preferenceKey);
        mExecutor = Executors.newSingleThreadExecutor();
        mBluetoothManager = Utils.getLocalBtManager(mContext);
        mLeBroadcastAssistant = getLeBroadcastAssistant(mBluetoothManager);
        mBroadcastAssistantCallback = new AudioStreamsBroadcastAssistantCallback(this);
    }

    @Override
    public int getAvailabilityStatus() {
        return AVAILABLE;
    }

    @Override
    public void displayPreference(PreferenceScreen screen) {
        super.displayPreference(screen);
        mCategoryPreference = screen.findPreference(getPreferenceKey());
    }

    @Override
    public void onStart(@NonNull LifecycleOwner owner) {
        if (mLeBroadcastAssistant == null) {
            Log.w(TAG, "onStart(): LeBroadcastAssistant is null!");
            return;
        }
        mBroadcastIdToPreferenceMap.clear();
        if (mCategoryPreference != null) {
            mCategoryPreference.removeAll();
        }
        mExecutor.execute(
                () -> {
                    mLeBroadcastAssistant.registerServiceCallBack(
                            mExecutor, mBroadcastAssistantCallback);
                    if (DEBUG) {
                        Log.d(TAG, "scanAudioStreamsStart()");
                    }
                    mLeBroadcastAssistant.startSearchingForSources(emptyList());
                    // Display currently connected streams
                    var unused =
                            ThreadUtils.postOnBackgroundThread(
                                    () -> {
                                        for (var sink :
                                                getActiveSinksOnAssistant(mBluetoothManager)) {
                                            mLeBroadcastAssistant
                                                    .getAllSources(sink)
                                                    .forEach(this::addSourceConnected);
                                        }
                                    });
                });
    }

    @Override
    public void onStop(@NonNull LifecycleOwner owner) {
        if (mLeBroadcastAssistant == null) {
            Log.w(TAG, "onStop(): LeBroadcastAssistant is null!");
            return;
        }
        mExecutor.execute(
                () -> {
                    if (mLeBroadcastAssistant.isSearchInProgress()) {
                        if (DEBUG) {
                            Log.d(TAG, "scanAudioStreamsStop()");
                        }
                        mLeBroadcastAssistant.stopSearchingForSources();
                    }
                    mLeBroadcastAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback);
                });
    }

    void setScanning(boolean isScanning) {
        ThreadUtils.postOnMainThread(
                () -> {
                    if (mCategoryPreference != null) mCategoryPreference.setProgress(isScanning);
                });
    }

    void addSourceFound(BluetoothLeBroadcastMetadata source) {
        Preference.OnPreferenceClickListener onClickListener =
                preference -> {
                    if (DEBUG) {
                        Log.d(TAG, "preferenceClicked(): attempt to join broadcast");
                    }

                    // TODO(chelseahao): add source to sink
                    return true;
                };
        mBroadcastIdToPreferenceMap.computeIfAbsent(
                source.getBroadcastId(),
                k -> {
                    var p = createPreference(source, onClickListener);
                    ThreadUtils.postOnMainThread(
                            () -> {
                                if (mCategoryPreference != null) {
                                    mCategoryPreference.addPreference(p);
                                }
                            });
                    return p;
                });
    }

    void removeSourceLost(int broadcastId) {
        var toRemove = mBroadcastIdToPreferenceMap.remove(broadcastId);
        if (toRemove != null) {
            ThreadUtils.postOnMainThread(
                    () -> {
                        if (mCategoryPreference != null) {
                            mCategoryPreference.removePreference(toRemove);
                        }
                    });
        }
        // TODO(chelseahao): remove source from sink
    }

    private void addSourceConnected(BluetoothLeBroadcastReceiveState state) {
        mBroadcastIdToPreferenceMap.compute(
                state.getBroadcastId(),
                (k, v) -> {
                    if (v == null) {
                        // Create a new preference as the source has not been added.
                        var p = createPreference(state);
                        ThreadUtils.postOnMainThread(
                                () -> {
                                    if (mCategoryPreference != null) {
                                        mCategoryPreference.addPreference(p);
                                    }
                                });
                        return p;
                    } else {
                        // This source has been added either by scanning, or it's currently
                        // connected to another active sink. Update its connection status to true
                        // if needed.
                        ThreadUtils.postOnMainThread(() -> v.setIsConnected(true, null));
                        return v;
                    }
                });
    }

    private AudioStreamPreference createPreference(
            BluetoothLeBroadcastMetadata source,
            Preference.OnPreferenceClickListener onPreferenceClickListener) {
        AudioStreamPreference preference = new AudioStreamPreference(mContext, /* attrs= */ null);
        preference.setTitle(
                source.getSubgroups().stream()
                        .map(s -> s.getContentMetadata().getProgramInfo())
                        .filter(i -> !Strings.isNullOrEmpty(i))
                        .findFirst()
                        .orElse("Broadcast Id: " + source.getBroadcastId()));
        preference.setIsConnected(false, onPreferenceClickListener);
        return preference;
    }

    private AudioStreamPreference createPreference(BluetoothLeBroadcastReceiveState state) {
        AudioStreamPreference preference = new AudioStreamPreference(mContext, /* attrs= */ null);
        preference.setTitle(
                state.getSubgroupMetadata().stream()
                        .map(BluetoothLeAudioContentMetadata::getProgramInfo)
                        .filter(i -> !Strings.isNullOrEmpty(i))
                        .findFirst()
                        .orElse("Broadcast Id: " + state.getBroadcastId()));
        preference.setIsConnected(true, null);
        return preference;
    }

    private static List<BluetoothDevice> getActiveSinksOnAssistant(LocalBluetoothManager manager) {
        if (manager == null) {
            Log.w(TAG, "getActiveSinksOnAssistant(): LocalBluetoothManager is null!");
            return emptyList();
        }
        return AudioSharingUtils.getActiveSinkOnAssistant(manager)
                .map(
                        cachedBluetoothDevice ->
                                Stream.concat(
                                                Stream.of(cachedBluetoothDevice.getDevice()),
                                                cachedBluetoothDevice.getMemberDevice().stream()
                                                        .map(CachedBluetoothDevice::getDevice))
                                        .toList())
                .orElse(emptyList());
    }

    private static @Nullable LocalBluetoothLeBroadcastAssistant getLeBroadcastAssistant(
            LocalBluetoothManager manager) {
        if (manager == null) {
            Log.w(TAG, "getLeBroadcastAssistant(): LocalBluetoothManager is null!");
            return null;
        }

        LocalBluetoothProfileManager profileManager = manager.getProfileManager();
        if (profileManager == null) {
            Log.w(TAG, "getLeBroadcastAssistant(): LocalBluetoothProfileManager is null!");
            return null;
        }

        return profileManager.getLeAudioBroadcastAssistantProfile();
    }

    void showToast(String msg) {
        AudioSharingUtils.toastMessage(mContext, msg);
    }
}