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

Commit 2f2afa22 authored by Yiyi Shen's avatar Yiyi Shen
Browse files

[Audiosharing] Add audio sharing section to Connected devices page.

The section will show up during a sharing session.

Flagged with enable_le_audio_sharing

Bug: 305620450
Test: Manual
Change-Id: I59cf81b35dcbf328b253d72d7fdc86af450251ee
parent 87372de0
Loading
Loading
Loading
Loading
+13 −0
Original line number Diff line number Diff line
@@ -26,6 +26,19 @@
        settings:controller="com.android.settings.slices.SlicePreferenceController"
        settings:allowDividerBelow="true"/>

    <PreferenceCategory
        android:key="audio_sharing_device_list"
        android:title="@string/audio_sharing_title"
        settings:controller="com.android.settings.connecteddevice.audiosharing.AudioSharingDevicePreferenceController">
        <Preference
            android:fragment="com.android.settings.connecteddevice.audiosharing.AudioSharingDashboardFragment"
            android:key="connected_device_audio_sharing_settings"
            android:title="@string/audio_sharing_title"
            android:icon="@drawable/ic_bt_audio_sharing"
            android:order="10"
            settings:controller="com.android.settings.connecteddevice.audiosharing.AudioSharingPreferenceController"/>
    </PreferenceCategory>

    <PreferenceCategory
        android:key="available_device_list"
        android:title="@string/connected_device_media_device_title"
+29 −18
Original line number Diff line number Diff line
@@ -27,8 +27,10 @@ import androidx.annotation.VisibleForTesting;
import com.android.settings.R;
import com.android.settings.SettingsActivity;
import com.android.settings.Utils;
import com.android.settings.connecteddevice.audiosharing.AudioSharingDevicePreferenceController;
import com.android.settings.core.SettingsUIDeviceConfig;
import com.android.settings.dashboard.DashboardFragment;
import com.android.settings.flags.Flags;
import com.android.settings.overlay.FeatureFactory;
import com.android.settings.overlay.SurveyFeatureProvider;
import com.android.settings.search.BaseSearchIndexProvider;
@@ -43,10 +45,8 @@ public class ConnectedDeviceDashboardFragment extends DashboardFragment {
    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
    private static final String SLICE_ACTION = "com.android.settings.SEARCH_RESULT_TRAMPOLINE";

    @VisibleForTesting
    static final String KEY_CONNECTED_DEVICES = "connected_device_list";
    @VisibleForTesting
    static final String KEY_AVAILABLE_DEVICES = "available_device_list";
    @VisibleForTesting static final String KEY_CONNECTED_DEVICES = "connected_device_list";
    @VisibleForTesting static final String KEY_AVAILABLE_DEVICES = "available_device_list";

    @Override
    public int getMetricsCategory() {
@@ -71,19 +71,31 @@ public class ConnectedDeviceDashboardFragment extends DashboardFragment {
    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        final boolean nearbyEnabled = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SETTINGS_UI,
                SettingsUIDeviceConfig.BT_NEAR_BY_SUGGESTION_ENABLED, true);
        String callingAppPackageName = ((SettingsActivity) getActivity())
                .getInitialCallingPackage();
        final boolean nearbyEnabled =
                DeviceConfig.getBoolean(
                        DeviceConfig.NAMESPACE_SETTINGS_UI,
                        SettingsUIDeviceConfig.BT_NEAR_BY_SUGGESTION_ENABLED,
                        true);
        String callingAppPackageName =
                ((SettingsActivity) getActivity()).getInitialCallingPackage();
        String action = getIntent() != null ? getIntent().getAction() : "";
        if (DEBUG) {
            Log.d(TAG, "onAttach() calling package name is : " + callingAppPackageName
                    + ", action : " + action);
            Log.d(
                    TAG,
                    "onAttach() calling package name is : "
                            + callingAppPackageName
                            + ", action : "
                            + action);
        }
        if (Flags.enableLeAudioSharing()) {
            use(AudioSharingDevicePreferenceController.class).init(this);
        }
        use(AvailableMediaDeviceGroupController.class).init(this);
        use(ConnectedDeviceGroupController.class).init(this);
        use(PreviouslyConnectedDevicePreferenceController.class).init(this);
        use(SlicePreferenceController.class).setSliceUri(nearbyEnabled
        use(SlicePreferenceController.class)
                .setSliceUri(
                        nearbyEnabled
                                ? Uri.parse(getString(R.string.config_nearby_devices_slice_uri))
                                : null);
        use(DiscoverableFooterPreferenceController.class)
@@ -102,14 +114,13 @@ public class ConnectedDeviceDashboardFragment extends DashboardFragment {

    @VisibleForTesting
    boolean isAlwaysDiscoverable(String callingAppPackageName, String action) {
        return TextUtils.equals(SLICE_ACTION, action) ? false
        return TextUtils.equals(SLICE_ACTION, action)
                ? false
                : TextUtils.equals(Utils.SETTINGS_PACKAGE_NAME, callingAppPackageName)
                        || TextUtils.equals(Utils.SYSTEMUI_PACKAGE_NAME, callingAppPackageName);
    }

    /**
     * For Search.
     */
    /** For Search. */
    public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =
            new BaseSearchIndexProvider(R.xml.connected_devices);
}
+112 −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;

import android.bluetooth.BluetoothLeBroadcastReceiveState;
import android.content.Context;
import android.util.Log;

import androidx.preference.Preference;

import com.android.settings.bluetooth.BluetoothDevicePreference;
import com.android.settings.bluetooth.BluetoothDeviceUpdater;
import com.android.settings.bluetooth.Utils;
import com.android.settings.connecteddevice.DevicePreferenceCallback;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
import com.android.settingslib.bluetooth.LocalBluetoothManager;

import java.util.List;

public class AudioSharingBluetoothDeviceUpdater extends BluetoothDeviceUpdater
        implements Preference.OnPreferenceClickListener {

    private static final String TAG = "AudioSharingBluetoothDeviceUpdater";

    private static final String PREF_KEY = "audio_sharing_bt";

    private LocalBluetoothManager mLocalBluetoothManager;

    public AudioSharingBluetoothDeviceUpdater(
            Context context,
            DevicePreferenceCallback devicePreferenceCallback,
            int metricsCategory) {
        super(context, devicePreferenceCallback, metricsCategory);
        mLocalBluetoothManager = Utils.getLocalBluetoothManager(context);
    }

    @Override
    public boolean isFilterMatched(CachedBluetoothDevice cachedDevice) {
        boolean isFilterMatched = false;
        if (isDeviceConnected(cachedDevice) && isDeviceInCachedDevicesList(cachedDevice)) {
            // If device is LE audio device and has a broadcast source,
            // it would show in audio sharing devices group.
            if (cachedDevice.isConnectedLeAudioDevice() && hasBroadcastSource(cachedDevice)) {
                isFilterMatched = true;
            }
        }
        Log.d(
                TAG,
                "isFilterMatched() device : "
                        + cachedDevice.getName()
                        + ", isFilterMatched : "
                        + isFilterMatched);
        return isFilterMatched;
    }

    @Override
    public boolean onPreferenceClick(Preference preference) {
        mMetricsFeatureProvider.logClickedPreference(preference, mMetricsCategory);
        final CachedBluetoothDevice device =
                ((BluetoothDevicePreference) preference).getBluetoothDevice();
        return device.setActive();
    }

    private boolean hasBroadcastSource(CachedBluetoothDevice cachedDevice) {
        LocalBluetoothLeBroadcastAssistant assistant =
                mLocalBluetoothManager.getProfileManager().getLeAudioBroadcastAssistantProfile();
        if (assistant == null) {
            return false;
        }
        List<BluetoothLeBroadcastReceiveState> sourceList =
                assistant.getAllSources(cachedDevice.getDevice());
        if (!sourceList.isEmpty()) return true;
        // Return true if member device is in broadcast.
        for (CachedBluetoothDevice device : cachedDevice.getMemberDevice()) {
            List<BluetoothLeBroadcastReceiveState> list =
                    assistant.getAllSources(device.getDevice());
            if (!list.isEmpty()) return true;
        }
        return false;
    }

    @Override
    protected String getPreferenceKey() {
        return PREF_KEY;
    }

    @Override
    protected String getLogTag() {
        return TAG;
    }

    @Override
    protected void update(CachedBluetoothDevice cachedBluetoothDevice) {
        super.update(cachedBluetoothDevice);
        Log.d(TAG, "Map : " + mPreferenceMap);
    }
}
+260 −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;

import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothLeBroadcastAssistant;
import android.bluetooth.BluetoothLeBroadcastMetadata;
import android.bluetooth.BluetoothLeBroadcastReceiveState;
import android.content.Context;
import android.content.pm.PackageManager;
import android.util.Log;

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

import com.android.settings.bluetooth.BluetoothDeviceUpdater;
import com.android.settings.bluetooth.Utils;
import com.android.settings.connecteddevice.DevicePreferenceCallback;
import com.android.settings.core.BasePreferenceController;
import com.android.settings.dashboard.DashboardFragment;
import com.android.settings.flags.Flags;
import com.android.settingslib.bluetooth.BluetoothCallback;
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
import com.android.settingslib.bluetooth.LocalBluetoothManager;

import java.util.concurrent.Executor;
import java.util.concurrent.Executors;

public class AudioSharingDevicePreferenceController extends BasePreferenceController
        implements DefaultLifecycleObserver, DevicePreferenceCallback, BluetoothCallback {

    private static final String TAG = "AudioSharingDevicePrefController";
    private static final String KEY = "audio_sharing_device_list";
    private static final String KEY_AUDIO_SHARING_SETTINGS =
            "connected_device_audio_sharing_settings";

    private final LocalBluetoothManager mLocalBtManager;
    private final LocalBluetoothLeBroadcastAssistant mAssistant;
    private final Executor mExecutor;
    private PreferenceGroup mPreferenceGroup;
    private Preference mAudioSharingSettingsPreference;
    private BluetoothDeviceUpdater mBluetoothDeviceUpdater;

    private BluetoothLeBroadcastAssistant.Callback mBroadcastAssistantCallback =
            new BluetoothLeBroadcastAssistant.Callback() {
                @Override
                public void onSearchStarted(int reason) {}

                @Override
                public void onSearchStartFailed(int reason) {}

                @Override
                public void onSearchStopped(int reason) {}

                @Override
                public void onSearchStopFailed(int reason) {}

                @Override
                public void onSourceFound(@NonNull BluetoothLeBroadcastMetadata source) {}

                @Override
                public void onSourceAdded(@NonNull BluetoothDevice sink, int sourceId, int reason) {
                    Log.d(
                            TAG,
                            "onSourceAdded(), sink = "
                                    + sink
                                    + ", sourceId = "
                                    + sourceId
                                    + ", reason = "
                                    + reason);
                    mBluetoothDeviceUpdater.forceUpdate();
                }

                @Override
                public void onSourceAddFailed(
                        @NonNull BluetoothDevice sink,
                        @NonNull BluetoothLeBroadcastMetadata source,
                        int reason) {
                    Log.d(
                            TAG,
                            "onSourceAddFailed(), sink = "
                                    + sink
                                    + ", source = "
                                    + source
                                    + ", reason = "
                                    + reason);
                }

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

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

                @Override
                public void onSourceRemoved(
                        @NonNull BluetoothDevice sink, int sourceId, int reason) {
                    Log.d(
                            TAG,
                            "onSourceRemoved(), sink = "
                                    + sink
                                    + ", sourceId = "
                                    + sourceId
                                    + ", reason = "
                                    + reason);
                    mBluetoothDeviceUpdater.forceUpdate();
                }

                @Override
                public void onSourceRemoveFailed(
                        @NonNull BluetoothDevice sink, int sourceId, int reason) {
                    Log.d(
                            TAG,
                            "onSourceRemoveFailed(), sink = "
                                    + sink
                                    + ", sourceId = "
                                    + sourceId
                                    + ", reason = "
                                    + reason);
                }

                @Override
                public void onReceiveStateChanged(
                        BluetoothDevice sink,
                        int sourceId,
                        BluetoothLeBroadcastReceiveState state) {}
            };

    public AudioSharingDevicePreferenceController(Context context) {
        super(context, KEY);
        mLocalBtManager = Utils.getLocalBtManager(mContext);
        mAssistant = mLocalBtManager.getProfileManager().getLeAudioBroadcastAssistantProfile();
        mExecutor = Executors.newSingleThreadExecutor();
    }

    @Override
    public void onStart(@NonNull LifecycleOwner owner) {
        if (mLocalBtManager == null) {
            Log.e(TAG, "onStart() Bluetooth is not supported on this device");
            return;
        }
        if (mAssistant == null) {
            Log.e(TAG, "onStart() Broadcast assistant is not supported on this device");
            return;
        }
        if (mBluetoothDeviceUpdater == null) {
            Log.e(TAG, "onStart() Bluetooth device updater is not initialized");
            return;
        }
        mLocalBtManager.getEventManager().registerCallback(this);
        mAssistant.registerServiceCallBack(mExecutor, mBroadcastAssistantCallback);
        mBluetoothDeviceUpdater.registerCallback();
        mBluetoothDeviceUpdater.refreshPreference();
    }

    @Override
    public void onStop(@NonNull LifecycleOwner owner) {
        if (mLocalBtManager == null) {
            Log.e(TAG, "onStop() Bluetooth is not supported on this device");
            return;
        }
        if (mAssistant == null) {
            Log.e(TAG, "onStop() Broadcast assistant is not supported on this device");
            return;
        }
        if (mBluetoothDeviceUpdater == null) {
            Log.e(TAG, "onStop() Bluetooth device updater is not initialized");
            return;
        }
        mLocalBtManager.getEventManager().unregisterCallback(this);
        // TODO: verify the reason for failing to unregister
        try {
            mAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback);
        } catch (IllegalArgumentException e) {
            Log.e(TAG, "Fail to unregister assistant callback due to " + e.getMessage());
        }
        mBluetoothDeviceUpdater.unregisterCallback();
    }

    @Override
    public void displayPreference(PreferenceScreen screen) {
        super.displayPreference(screen);

        mPreferenceGroup = screen.findPreference(KEY);
        mAudioSharingSettingsPreference =
                mPreferenceGroup.findPreference(KEY_AUDIO_SHARING_SETTINGS);
        mPreferenceGroup.setVisible(false);
        mAudioSharingSettingsPreference.setVisible(false);

        if (isAvailable()) {
            mBluetoothDeviceUpdater.setPrefContext(screen.getContext());
            mBluetoothDeviceUpdater.forceUpdate();
        }
    }

    @Override
    public int getAvailabilityStatus() {
        return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH)
                        && Flags.enableLeAudioSharing()
                ? AVAILABLE_UNSEARCHABLE
                : UNSUPPORTED_ON_DEVICE;
    }

    @Override
    public String getPreferenceKey() {
        return KEY;
    }

    @Override
    public void onDeviceAdded(Preference preference) {
        if (mPreferenceGroup.getPreferenceCount() == 1) {
            mPreferenceGroup.setVisible(true);
            mAudioSharingSettingsPreference.setVisible(true);
        }
        mPreferenceGroup.addPreference(preference);
    }

    @Override
    public void onDeviceRemoved(Preference preference) {
        mPreferenceGroup.removePreference(preference);
        if (mPreferenceGroup.getPreferenceCount() == 1) {
            mPreferenceGroup.setVisible(false);
            mAudioSharingSettingsPreference.setVisible(false);
        }
    }

    /**
     * Initialize the controller.
     *
     * @param fragment The fragment to provide the context and metrics category for {@link
     *     AudioSharingBluetoothDeviceUpdater}.
     */
    public void init(DashboardFragment fragment) {
        mBluetoothDeviceUpdater =
                new AudioSharingBluetoothDeviceUpdater(
                        fragment.getContext(),
                        AudioSharingDevicePreferenceController.this,
                        fragment.getMetricsCategory());
    }
}
+14 −3
Original line number Diff line number Diff line
@@ -56,6 +56,9 @@ public class ConnectedDeviceDashboardFragmentTest {
    private static final String KEY_FAST_PAIR_DEVICE_SEE_ALL = "fast_pair_devices_see_all";
    private static final String KEY_FAST_PAIR_DEVICE_LIST = "fast_pair_devices";
    private static final String KEY_ADD_BT_DEVICES = "add_bt_devices";
    private static final String KEY_AUDIO_SHARING_DEVICE_LIST = "audio_sharing_device_list";
    private static final String KEY_AUDIO_SHARING_SETTINGS =
            "connected_device_audio_sharing_settings";
    private static final String SETTINGS_PACKAGE_NAME = "com.android.settings";
    private static final String SYSTEMUI_PACKAGE_NAME = "com.android.systemui";
    private static final String SLICE_ACTION = "com.android.settings.SEARCH_RESULT_TRAMPOLINE";
@@ -93,9 +96,17 @@ public class ConnectedDeviceDashboardFragmentTest {
        final List<String> niks = ConnectedDeviceDashboardFragment.SEARCH_INDEX_DATA_PROVIDER
                .getNonIndexableKeys(mContext);

        assertThat(niks).containsExactly(KEY_CONNECTED_DEVICES, KEY_AVAILABLE_DEVICES,
                KEY_NEARBY_DEVICES, KEY_DISCOVERABLE_FOOTER, KEY_SAVED_DEVICE_SEE_ALL,
                KEY_FAST_PAIR_DEVICE_SEE_ALL, KEY_FAST_PAIR_DEVICE_LIST);
        assertThat(niks)
                .containsExactly(
                        KEY_CONNECTED_DEVICES,
                        KEY_AVAILABLE_DEVICES,
                        KEY_NEARBY_DEVICES,
                        KEY_DISCOVERABLE_FOOTER,
                        KEY_SAVED_DEVICE_SEE_ALL,
                        KEY_FAST_PAIR_DEVICE_SEE_ALL,
                        KEY_FAST_PAIR_DEVICE_LIST,
                        KEY_AUDIO_SHARING_DEVICE_LIST,
                        KEY_AUDIO_SHARING_SETTINGS);
    }

    @Test