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

Commit fd4c6d10 authored by Haijie Hong's avatar Haijie Hong Committed by Android (Google) Code Review
Browse files

Merge "Add audio sharing entrypoint in device details" into main

parents 17224598 cdcf50e3
Loading
Loading
Loading
Loading
+4 −0
Original line number Diff line number Diff line
@@ -68,6 +68,10 @@
        settings:controller="com.android.settings.slices.SlicePreferenceController"
        settings:allowDividerAbove="true"/>

    <PreferenceCategory
        android:key="audio_sharing_control"
        android:layout="@layout/settingslib_preference_category_no_title"/>

    <PreferenceCategory
        android:key="bt_device_slice_category"
        settings:controller="com.android.settings.bluetooth.BlockingPrefWithSliceController"/>
+132 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.bluetooth;

import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast.EXTRA_START_LE_AUDIO_SHARING;

import android.app.settings.SettingsEnums;
import android.content.Context;
import android.os.Bundle;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.preference.Preference;
import androidx.preference.PreferenceCategory;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceScreen;

import com.android.settings.R;
import com.android.settings.connecteddevice.audiosharing.AudioSharingDashboardFragment;
import com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsDashboardFragment;
import com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsHelper;
import com.android.settings.core.SubSettingLauncher;
import com.android.settingslib.bluetooth.BluetoothUtils;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.settingslib.core.lifecycle.Lifecycle;

/** Controller for audio sharing control preferences. */
public class BluetoothDetailsAudioSharingController extends BluetoothDetailsController {
    private static final String KEY_AUDIO_SHARING_CONTROL = "audio_sharing_control";
    private static final String KEY_AUDIO_SHARING = "audio_sharing";
    private static final String KEY_FIND_AUDIO_STREAM = "find_audio_stream";

    @Nullable PreferenceCategory mProfilesContainer;
    LocalBluetoothManager mLocalBluetoothManager;

    public BluetoothDetailsAudioSharingController(
            @NonNull Context context,
            @NonNull PreferenceFragmentCompat fragment,
            @NonNull LocalBluetoothManager localBtManager,
            @NonNull CachedBluetoothDevice device,
            @NonNull Lifecycle lifecycle) {
        super(context, fragment, device, lifecycle);
        mLocalBluetoothManager = localBtManager;
    }

    @Override
    public boolean isAvailable() {
        return BluetoothUtils.isAudioSharingUIAvailable(mContext)
                && mCachedDevice.isConnectedLeAudioDevice();
    }

    @Override
    protected void init(PreferenceScreen screen) {
        mProfilesContainer = screen.findPreference(KEY_AUDIO_SHARING_CONTROL);
    }

    @Override
    protected void refresh() {
        if (mProfilesContainer == null) {
            return;
        }
        if (!isAvailable()) {
            mProfilesContainer.setVisible(false);
            return;
        }
        mProfilesContainer.setVisible(true);
        mProfilesContainer.removeAll();
        mProfilesContainer.addPreference(createAudioSharingPreference());
        if ((BluetoothUtils.isActiveLeAudioDevice(mCachedDevice)
                        || AudioStreamsHelper.hasConnectedBroadcastSource(
                                mCachedDevice, mLocalBluetoothManager))
                && !BluetoothUtils.isBroadcasting(mLocalBluetoothManager)) {
            mProfilesContainer.addPreference(createFindAudioStreamPreference());
        }
    }

    private Preference createAudioSharingPreference() {
        Preference audioSharingPref = new Preference(mContext);
        audioSharingPref.setKey(KEY_AUDIO_SHARING);
        audioSharingPref.setTitle(R.string.audio_sharing_title);
        audioSharingPref.setIcon(com.android.settingslib.R.drawable.ic_bt_le_audio_sharing);
        audioSharingPref.setOnPreferenceClickListener(
                preference -> {
                    Bundle args = new Bundle();
                    args.putBoolean(EXTRA_START_LE_AUDIO_SHARING, true);
                    new SubSettingLauncher(mContext)
                            .setDestination(AudioSharingDashboardFragment.class.getName())
                            .setSourceMetricsCategory(SettingsEnums.BLUETOOTH_DEVICE_DETAILS)
                            .setArguments(args)
                            .launch();
                    return true;
                });
        return audioSharingPref;
    }

    private Preference createFindAudioStreamPreference() {
        Preference findAudioStreamPref = new Preference(mContext);
        findAudioStreamPref.setKey(KEY_FIND_AUDIO_STREAM);
        findAudioStreamPref.setTitle(R.string.audio_streams_main_page_title);
        findAudioStreamPref.setIcon(com.android.settingslib.R.drawable.ic_bt_le_audio_sharing);
        findAudioStreamPref.setOnPreferenceClickListener(
                preference -> {
                    new SubSettingLauncher(mContext)
                            .setDestination(AudioStreamsDashboardFragment.class.getName())
                            .setSourceMetricsCategory(SettingsEnums.BLUETOOTH_DEVICE_DETAILS)
                            .launch();
                    return true;
                });
        return findAudioStreamPref;
    }

    @Override
    @NonNull
    public String getPreferenceKey() {
        return KEY_AUDIO_SHARING_CONTROL;
    }
}
+3 −0
Original line number Diff line number Diff line
@@ -440,6 +440,9 @@ public class BluetoothDeviceDetailsFragment extends RestrictedDashboardFragment
                            context, this, mCachedDevice, lifecycle));
            controllers.add(new BluetoothDetailsButtonsController(context, this, mCachedDevice,
                    lifecycle));
            controllers.add(
                    new BluetoothDetailsAudioSharingController(
                            context, this, mManager, mCachedDevice, lifecycle));
            controllers.add(new BluetoothDetailsCompanionAppsController(context, this,
                    mCachedDevice, lifecycle));
            controllers.add(new BluetoothDetailsAudioDeviceTypeController(context, this, mManager,
+1 −1
Original line number Diff line number Diff line
@@ -269,7 +269,7 @@ public class AudioStreamsHelper {
     * @param localBtManager The BT manager to provide BT functions.
     * @return Whether the device has connected to a broadcast source.
     */
    private static boolean hasConnectedBroadcastSource(
    public static boolean hasConnectedBroadcastSource(
            CachedBluetoothDevice cachedDevice, LocalBluetoothManager localBtManager) {
        if (localBtManager == null) {
            Log.d(TAG, "Skip check hasConnectedBroadcastSource due to bt manager is null");
+169 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.bluetooth;

import static com.google.common.truth.Truth.assertThat;

import static org.mockito.Mockito.when;

import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothLeBroadcastReceiveState;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothStatusCodes;
import android.platform.test.annotations.EnableFlags;
import android.platform.test.flag.junit.SetFlagsRule;

import androidx.preference.PreferenceCategory;

import com.android.settings.R;
import com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsHelper;
import com.android.settings.connecteddevice.audiosharing.audiostreams.testshadows.ShadowAudioStreamsHelper;
import com.android.settings.testutils.shadow.ShadowBluetoothAdapter;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.settingslib.flags.Flags;

import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Answers;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import org.robolectric.shadow.api.Shadow;

import java.util.List;

@RunWith(RobolectricTestRunner.class)
@Config(shadows = {ShadowBluetoothAdapter.class, ShadowAudioStreamsHelper.class})
public class BluetoothDetailsAudioSharingControllerTest extends BluetoothDetailsControllerTestBase {
    @Rule public final MockitoRule mocks = MockitoJUnit.rule();
    @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();

    @Mock(answer = Answers.RETURNS_DEEP_STUBS)
    private LocalBluetoothManager mLocalManager;
    @Mock private AudioStreamsHelper mAudioStreamsHelper;
    @Mock private BluetoothLeBroadcastReceiveState mBroadcastReceiveState;

    private ShadowBluetoothAdapter mShadowBluetoothAdapter;
    private BluetoothDetailsAudioSharingController mController;
    private PreferenceCategory mContainer;

    @Override
    public void setUp() {
        super.setUp();
        mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter());
        ShadowAudioStreamsHelper.setUseMock(mAudioStreamsHelper);
        mController =
                new BluetoothDetailsAudioSharingController(
                        mContext, mFragment, mLocalManager, mCachedDevice, mLifecycle);
        mContainer = new PreferenceCategory(mContext);
        mContainer.setKey(mController.getPreferenceKey());
        mScreen.addPreference(mContainer);
        setupDevice(mDeviceConfig);
    }

    @Test
    @EnableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING)
    public void notConnected_noAudioSharingPreferences() {
        when(mCachedDevice.isConnectedLeAudioDevice()).thenReturn(false);

        showScreen(mController);

        assertThat(mContainer.isVisible()).isFalse();
    }

    @Test
    @EnableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING)
    public void connected_showOnePreference() {
        when(mCachedDevice.isConnectedLeAudioDevice()).thenReturn(true);
        when(mCachedDevice.isActiveDevice(BluetoothProfile.LE_AUDIO)).thenReturn(false);
        when(mLocalManager
                        .getProfileManager()
                        .getLeAudioBroadcastAssistantProfile()
                        .getAllSources(mDevice))
                .thenReturn(List.of());
        when(mLocalManager
                .getProfileManager()
                .getLeAudioBroadcastProfile()
                .isEnabled(mDevice))
                .thenReturn(true);
        mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
                BluetoothStatusCodes.FEATURE_SUPPORTED);
        mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
                BluetoothStatusCodes.FEATURE_SUPPORTED);

        showScreen(mController);

        assertThat(mContainer.isVisible()).isTrue();
        assertThat(mContainer.getPreferenceCount()).isEqualTo(1);
        assertThat(mContainer.getPreference(0).getTitle())
                .isEqualTo(mContext.getString(R.string.audio_sharing_title));
    }

    @Test
    @EnableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING)
    public void connected_active_showTwoPreference() {
        when(mCachedDevice.isConnectedLeAudioDevice()).thenReturn(true);
        when(mCachedDevice.isActiveDevice(BluetoothProfile.LE_AUDIO)).thenReturn(true);
        when(mLocalManager
                .getProfileManager()
                .getLeAudioBroadcastAssistantProfile()
                .getAllSources(mDevice))
                .thenReturn(List.of());
        when(mLocalManager
                .getProfileManager()
                .getLeAudioBroadcastProfile()
                .isEnabled(mDevice))
                .thenReturn(false);
        mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
                BluetoothStatusCodes.FEATURE_SUPPORTED);
        mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
                BluetoothStatusCodes.FEATURE_SUPPORTED);

        showScreen(mController);

        assertThat(mContainer.isVisible()).isTrue();
        assertThat(mContainer.getPreferenceCount()).isEqualTo(2);
    }

    @Test
    @EnableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING)
    public void connected_hasConnectedBroadcastSource_showTwoPreference() {
        when(mCachedDevice.isConnectedLeAudioDevice()).thenReturn(true);
        when(mCachedDevice.isActiveDevice(BluetoothProfile.LE_AUDIO)).thenReturn(false);
        when(mLocalManager
                .getProfileManager()
                .getLeAudioBroadcastAssistantProfile()
                .getAllSources(mDevice))
                .thenReturn(List.of(mBroadcastReceiveState));
        when(mLocalManager
                .getProfileManager()
                .getLeAudioBroadcastProfile()
                .isEnabled(mDevice))
                .thenReturn(false);
        mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
                BluetoothStatusCodes.FEATURE_SUPPORTED);
        mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
                BluetoothStatusCodes.FEATURE_SUPPORTED);

        showScreen(mController);

        assertThat(mContainer.isVisible()).isTrue();
        assertThat(mContainer.getPreferenceCount()).isEqualTo(2);
    }
}