Merge changes from topic "join_handler" into main

* changes:
  [Audiosharing] Handle device connected in handler activity
  [Audiosharing] Add activity skeleton to handle new connected sink
This commit is contained in:
Yiyi Shen
2025-02-26 23:02:05 -08:00
committed by Android (Google) Code Review
7 changed files with 866 additions and 0 deletions

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:settings="http://schemas.android.com/apk/res-auto"
android:key="audio_sharing_join_handler"
settings:searchable="false"
settings:controller="com.android.settings.connecteddevice.audiosharing.AudioSharingJoinHandlerController" />

View File

@@ -0,0 +1,46 @@
/*
* 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.connecteddevice.audiosharing;
import android.os.Bundle;
import com.android.settings.SettingsActivity;
import com.android.settingslib.bluetooth.BluetoothUtils;
import com.android.settingslib.flags.Flags;
public class AudioSharingJoinHandlerActivity extends SettingsActivity {
private static final String TAG = "AudioSharingJoinHandlerActivity";
@Override
protected void onCreate(Bundle savedState) {
super.onCreate(savedState);
if (!Flags.promoteAudioSharingForSecondAutoConnectedLeaDevice()
|| !BluetoothUtils.isAudioSharingUIAvailable(this)) {
finish();
}
}
@Override
protected boolean isToolbarEnabled() {
return false;
}
@Override
protected boolean isValidFragment(String fragmentName) {
return AudioSharingJoinHandlerDashboardFragment.class.getName().equals(fragmentName);
}
}

View File

@@ -0,0 +1,274 @@
/*
* 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.connecteddevice.audiosharing;
import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast.EXTRA_BLUETOOTH_DEVICE;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothLeBroadcastAssistant;
import android.bluetooth.BluetoothLeBroadcastMetadata;
import android.bluetooth.BluetoothLeBroadcastReceiveState;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.annotation.WorkerThread;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
import androidx.preference.PreferenceScreen;
import com.android.settings.bluetooth.Utils;
import com.android.settings.core.BasePreferenceController;
import com.android.settings.dashboard.DashboardFragment;
import com.android.settingslib.bluetooth.BluetoothCallback;
import com.android.settingslib.bluetooth.BluetoothEventManager;
import com.android.settingslib.bluetooth.BluetoothUtils;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager;
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.settingslib.flags.Flags;
import com.android.settingslib.utils.ThreadUtils;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
public class AudioSharingJoinHandlerController extends BasePreferenceController
implements DefaultLifecycleObserver, BluetoothCallback {
private static final String TAG = "AudioSharingJoinHandlerCtrl";
private static final String KEY = "audio_sharing_join_handler";
@Nullable private final LocalBluetoothManager mBtManager;
@Nullable private final BluetoothEventManager mEventManager;
@Nullable private final CachedBluetoothDeviceManager mDeviceManager;
@Nullable private final LocalBluetoothLeBroadcastAssistant mAssistant;
private final Executor mExecutor;
@Nullable private DashboardFragment mFragment;
@Nullable private AudioSharingDialogHandler mDialogHandler;
@VisibleForTesting
BluetoothLeBroadcastAssistant.Callback mAssistantCallback =
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: dismiss stale dialog.");
if (mDeviceManager != null && mDialogHandler != null) {
CachedBluetoothDevice cachedDevice = mDeviceManager.findDevice(sink);
if (cachedDevice != null) {
mDialogHandler.closeOpeningDialogsForLeaDevice(cachedDevice);
}
}
}
@Override
public void onSourceAddFailed(
@NonNull BluetoothDevice sink,
@NonNull BluetoothLeBroadcastMetadata source,
int 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) {
}
@Override
public void onSourceRemoveFailed(
@NonNull BluetoothDevice sink, int sourceId, int reason) {
}
@Override
public void onReceiveStateChanged(
@NonNull BluetoothDevice sink,
int sourceId,
@NonNull BluetoothLeBroadcastReceiveState state) {
}
};
public AudioSharingJoinHandlerController(@NonNull Context context,
@NonNull String preferenceKey) {
super(context, preferenceKey);
mBtManager = Utils.getLocalBtManager(mContext);
mEventManager = mBtManager == null ? null : mBtManager.getEventManager();
mDeviceManager = mBtManager == null ? null : mBtManager.getCachedDeviceManager();
mAssistant = mBtManager == null ? null
: mBtManager.getProfileManager().getLeAudioBroadcastAssistantProfile();
mExecutor = Executors.newSingleThreadExecutor();
}
/**
* Initialize the controller.
*
* @param fragment The fragment to provide the context and metrics category for {@link
* AudioSharingBluetoothDeviceUpdater} and provide the host for dialogs.
*/
public void init(@NonNull DashboardFragment fragment) {
mFragment = fragment;
mDialogHandler = new AudioSharingDialogHandler(mContext, fragment);
}
@Override
public void onStart(@NonNull LifecycleOwner owner) {
var unused = ThreadUtils.postOnBackgroundThread(() -> {
if (!isAvailable()) {
Log.d(TAG, "Skip onStart(), feature is not supported.");
return;
}
if (mEventManager == null || mDialogHandler == null || mAssistant == null) {
Log.d(TAG, "Skip onStart(), profile is not ready.");
return;
}
Log.d(TAG, "onStart() Register callbacks.");
mEventManager.registerCallback(this);
mAssistant.registerServiceCallBack(mExecutor, mAssistantCallback);
mDialogHandler.registerCallbacks(mExecutor);
});
}
@Override
public void onStop(@NonNull LifecycleOwner owner) {
var unused = ThreadUtils.postOnBackgroundThread(() -> {
if (!isAvailable()) {
Log.d(TAG, "Skip onStop(), feature is not supported.");
return;
}
if (mEventManager == null || mDialogHandler == null || mAssistant == null) {
Log.d(TAG, "Skip onStop(), profile is not ready.");
return;
}
Log.d(TAG, "onStop() Unregister callbacks.");
mEventManager.unregisterCallback(this);
mAssistant.unregisterServiceCallBack(mAssistantCallback);
mDialogHandler.unregisterCallbacks();
});
}
@Override
public int getAvailabilityStatus() {
return (Flags.promoteAudioSharingForSecondAutoConnectedLeaDevice()
&& BluetoothUtils.isAudioSharingUIAvailable(mContext))
? AVAILABLE_UNSEARCHABLE
: UNSUPPORTED_ON_DEVICE;
}
@Override
public String getPreferenceKey() {
return KEY;
}
@Override
public int getSliceHighlightMenuRes() {
return 0;
}
@Override
public void displayPreference(@NonNull PreferenceScreen screen) {
super.displayPreference(screen);
if (mFragment == null
|| mFragment.getActivity() == null
|| mFragment.getActivity().getIntent() == null) {
Log.d(TAG, "Skip handleDeviceConnectedFromIntent, fragment intent is null");
return;
}
Intent intent = mFragment.getActivity().getIntent();
var unused =
ThreadUtils.postOnBackgroundThread(() -> handleDeviceConnectedFromIntent(intent));
}
@Override
public void onProfileConnectionStateChanged(
@NonNull CachedBluetoothDevice cachedDevice,
@ConnectionState int state,
int bluetoothProfile) {
if (mDialogHandler == null || mFragment == null) {
Log.d(TAG, "Ignore onProfileConnectionStateChanged, not init correctly");
return;
}
// Close related dialogs if the BT remote device is disconnected.
if (state == BluetoothAdapter.STATE_DISCONNECTED) {
boolean isLeAudioSupported = BluetoothUtils.isLeAudioSupported(cachedDevice);
if (isLeAudioSupported
&& bluetoothProfile == BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT) {
mDialogHandler.closeOpeningDialogsForLeaDevice(cachedDevice);
} else if (!isLeAudioSupported && !cachedDevice.isConnected()) {
mDialogHandler.closeOpeningDialogsForNonLeaDevice(cachedDevice);
}
}
}
/** Handle just connected device via intent. */
@WorkerThread
public void handleDeviceConnectedFromIntent(@NonNull Intent intent) {
BluetoothDevice device = intent.getParcelableExtra(EXTRA_BLUETOOTH_DEVICE,
BluetoothDevice.class);
CachedBluetoothDevice cachedDevice =
(device == null || mDeviceManager == null)
? null
: mDeviceManager.findDevice(device);
if (cachedDevice == null) {
Log.d(TAG, "Skip handleDeviceConnectedFromIntent, device is null");
return;
}
if (mDialogHandler == null) {
Log.d(TAG, "Skip handleDeviceConnectedFromIntent, handler is null");
return;
}
Log.d(TAG, "handleDeviceConnectedFromIntent, device = " + device.getAnonymizedAddress());
mDialogHandler.handleDeviceConnected(cachedDevice, /* userTriggered= */ false);
}
@VisibleForTesting
void setDialogHandler(@Nullable AudioSharingDialogHandler dialogHandler) {
mDialogHandler = dialogHandler;
}
}

View File

@@ -0,0 +1,55 @@
/*
* 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.connecteddevice.audiosharing;
import android.content.Context;
import androidx.annotation.Nullable;
import com.android.settings.R;
import com.android.settings.dashboard.DashboardFragment;
public class AudioSharingJoinHandlerDashboardFragment extends DashboardFragment {
private static final String TAG = "AudioSharingJoinHandlerFrag";
@Nullable private AudioSharingJoinHandlerController mController;
@Override
public int getMetricsCategory() {
// TODO: use real enum
return 0;
}
@Override
protected int getPreferenceScreenResId() {
return R.xml.bluetooth_le_audio_sharing_join_handler;
}
@Override
protected String getLogTag() {
return TAG;
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
mController = use(AudioSharingJoinHandlerController.class);
if (mController != null) {
mController.init(this);
}
}
}

View File

@@ -0,0 +1,94 @@
/*
* 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.connecteddevice.audiosharing;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothStatusCodes;
import android.os.Bundle;
import android.platform.test.annotations.DisableFlags;
import android.platform.test.annotations.EnableFlags;
import android.platform.test.flag.junit.SetFlagsRule;
import com.android.settings.testutils.shadow.ShadowBluetoothAdapter;
import com.android.settingslib.flags.Flags;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import org.robolectric.shadow.api.Shadow;
@RunWith(RobolectricTestRunner.class)
@Config(shadows = {ShadowBluetoothAdapter.class})
public class AudioSharingJoinHandlerActivityTest {
@Rule
public final MockitoRule mMockitoRule = MockitoJUnit.rule();
@Rule
public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
private ShadowBluetoothAdapter mShadowBluetoothAdapter;
private AudioSharingJoinHandlerActivity mActivity;
@Before
public void setUp() {
mActivity = spy(Robolectric.buildActivity(AudioSharingJoinHandlerActivity.class).get());
mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter());
mShadowBluetoothAdapter.setEnabled(true);
mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
BluetoothStatusCodes.FEATURE_SUPPORTED);
mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
BluetoothStatusCodes.FEATURE_SUPPORTED);
}
@Test
@DisableFlags(Flags.FLAG_PROMOTE_AUDIO_SHARING_FOR_SECOND_AUTO_CONNECTED_LEA_DEVICE)
public void onCreate_flagOff_finish() {
mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
mActivity.onCreate(new Bundle());
verify(mActivity).finish();
}
@Test
@EnableFlags({Flags.FLAG_ENABLE_LE_AUDIO_SHARING,
Flags.FLAG_PROMOTE_AUDIO_SHARING_FOR_SECOND_AUTO_CONNECTED_LEA_DEVICE})
public void onCreate_flagOn_create() {
mActivity.onCreate(new Bundle());
verify(mActivity, never()).finish();
}
@Test
public void isValidFragment_returnsTrue() {
assertThat(mActivity.isValidFragment(
AudioSharingJoinHandlerDashboardFragment.class.getName())).isTrue();
}
@Test
public void isValidFragment_returnsFalse() {
assertThat(mActivity.isValidFragment("")).isFalse();
}
}

View File

@@ -0,0 +1,326 @@
/*
* 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.connecteddevice.audiosharing;
import static com.android.settings.core.BasePreferenceController.AVAILABLE_UNSEARCHABLE;
import static com.android.settings.core.BasePreferenceController.UNSUPPORTED_ON_DEVICE;
import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast.EXTRA_BLUETOOTH_DEVICE;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
import static org.robolectric.Shadows.shadowOf;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothLeBroadcastAssistant;
import android.bluetooth.BluetoothLeBroadcastMetadata;
import android.bluetooth.BluetoothLeBroadcastReceiveState;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothStatusCodes;
import android.content.Context;
import android.content.Intent;
import android.os.Looper;
import android.platform.test.annotations.DisableFlags;
import android.platform.test.annotations.EnableFlags;
import android.platform.test.flag.junit.SetFlagsRule;
import androidx.fragment.app.FragmentActivity;
import androidx.lifecycle.LifecycleOwner;
import androidx.preference.PreferenceScreen;
import androidx.test.core.app.ApplicationProvider;
import com.android.settings.bluetooth.Utils;
import com.android.settings.dashboard.DashboardFragment;
import com.android.settings.testutils.shadow.ShadowBluetoothAdapter;
import com.android.settings.testutils.shadow.ShadowBluetoothUtils;
import com.android.settings.testutils.shadow.ShadowFragment;
import com.android.settingslib.bluetooth.BluetoothCallback;
import com.android.settingslib.bluetooth.BluetoothEventManager;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager;
import com.android.settingslib.bluetooth.LeAudioProfile;
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
import com.android.settingslib.core.lifecycle.Lifecycle;
import com.android.settingslib.flags.Flags;
import com.google.common.collect.ImmutableList;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Spy;
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.concurrent.Executor;
@RunWith(RobolectricTestRunner.class)
@Config(shadows = {
ShadowBluetoothAdapter.class,
ShadowBluetoothUtils.class,
ShadowFragment.class,
})
public class AudioSharingJoinHandlerControllerTest {
private static final String PREF_KEY = "audio_sharing_join_handler";
@Rule
public final MockitoRule mMockitoRule = MockitoJUnit.rule();
@Rule
public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
@Spy
Context mContext = ApplicationProvider.getApplicationContext();
private Lifecycle mLifecycle;
private LifecycleOwner mLifecycleOwner;
@Mock
private LocalBluetoothManager mLocalBtManager;
@Mock private BluetoothEventManager mEventManager;
@Mock private LocalBluetoothProfileManager mProfileManager;
@Mock private CachedBluetoothDeviceManager mDeviceManager;
@Mock private LocalBluetoothLeBroadcastAssistant mAssistant;
@Mock private PreferenceScreen mScreen;
@Mock private DashboardFragment mFragment;
@Mock private FragmentActivity mActivity;
@Mock private AudioSharingDialogHandler mDialogHandler;
private AudioSharingJoinHandlerController mController;
@Before
public void setUp() {
ShadowBluetoothAdapter shadowBluetoothAdapter =
Shadow.extract(BluetoothAdapter.getDefaultAdapter());
shadowBluetoothAdapter.setEnabled(true);
shadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
BluetoothStatusCodes.FEATURE_SUPPORTED);
shadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
BluetoothStatusCodes.FEATURE_SUPPORTED);
mLifecycleOwner = () -> mLifecycle;
mLifecycle = new Lifecycle(mLifecycleOwner);
ShadowBluetoothUtils.sLocalBluetoothManager = mLocalBtManager;
mLocalBtManager = Utils.getLocalBtManager(mContext);
when(mLocalBtManager.getEventManager()).thenReturn(mEventManager);
when(mLocalBtManager.getProfileManager()).thenReturn(mProfileManager);
when(mLocalBtManager.getCachedDeviceManager()).thenReturn(mDeviceManager);
when(mProfileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(mAssistant);
mController = new AudioSharingJoinHandlerController(mContext, PREF_KEY);
doReturn(mActivity).when(mFragment).getActivity();
mController.init(mFragment);
mController.setDialogHandler(mDialogHandler);
}
@After
public void tearDown() {
ShadowBluetoothUtils.reset();
}
@Test
@DisableFlags(Flags.FLAG_PROMOTE_AUDIO_SHARING_FOR_SECOND_AUTO_CONNECTED_LEA_DEVICE)
public void onStart_flagOff_doNothing() {
mController.onStart(mLifecycleOwner);
verify(mEventManager, never()).registerCallback(any(BluetoothCallback.class));
verify(mDialogHandler, never()).registerCallbacks(any(Executor.class));
verify(mAssistant, never())
.registerServiceCallBack(
any(Executor.class), any(BluetoothLeBroadcastAssistant.Callback.class));
}
@Test
@EnableFlags({Flags.FLAG_ENABLE_LE_AUDIO_SHARING,
Flags.FLAG_PROMOTE_AUDIO_SHARING_FOR_SECOND_AUTO_CONNECTED_LEA_DEVICE})
public void onStart_flagOn_registerCallbacks() {
mController.onStart(mLifecycleOwner);
verify(mEventManager).registerCallback(any(BluetoothCallback.class));
verify(mDialogHandler).registerCallbacks(any(Executor.class));
verify(mAssistant)
.registerServiceCallBack(
any(Executor.class), any(BluetoothLeBroadcastAssistant.Callback.class));
}
@Test
@DisableFlags(Flags.FLAG_PROMOTE_AUDIO_SHARING_FOR_SECOND_AUTO_CONNECTED_LEA_DEVICE)
public void onStop_flagOff_doNothing() {
mController.onStop(mLifecycleOwner);
verify(mEventManager, never()).unregisterCallback(any(BluetoothCallback.class));
verify(mDialogHandler, never()).unregisterCallbacks();
verify(mAssistant, never())
.unregisterServiceCallBack(any(BluetoothLeBroadcastAssistant.Callback.class));
}
@Test
@EnableFlags({Flags.FLAG_ENABLE_LE_AUDIO_SHARING,
Flags.FLAG_PROMOTE_AUDIO_SHARING_FOR_SECOND_AUTO_CONNECTED_LEA_DEVICE})
public void onStop_flagOn_unregisterCallbacks() {
mController.onStop(mLifecycleOwner);
verify(mEventManager).unregisterCallback(any(BluetoothCallback.class));
verify(mDialogHandler).unregisterCallbacks();
verify(mAssistant)
.unregisterServiceCallBack(any(BluetoothLeBroadcastAssistant.Callback.class));
}
@Test
@EnableFlags({Flags.FLAG_PROMOTE_AUDIO_SHARING_FOR_SECOND_AUTO_CONNECTED_LEA_DEVICE,
Flags.FLAG_ENABLE_LE_AUDIO_SHARING})
public void getAvailabilityStatus_flagOn() {
assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE_UNSEARCHABLE);
}
@Test
@DisableFlags(Flags.FLAG_PROMOTE_AUDIO_SHARING_FOR_SECOND_AUTO_CONNECTED_LEA_DEVICE)
public void getAvailabilityStatus_flagOff() {
assertThat(mController.getAvailabilityStatus()).isEqualTo(UNSUPPORTED_ON_DEVICE);
}
@Test
public void getPreferenceKey_returnsCorrectKey() {
assertThat(mController.getPreferenceKey()).isEqualTo(PREF_KEY);
}
@Test
public void getSliceHighlightMenuRes_returnsZero() {
assertThat(mController.getSliceHighlightMenuRes()).isEqualTo(0);
}
@Test
@EnableFlags({Flags.FLAG_ENABLE_LE_AUDIO_SHARING,
Flags.FLAG_PROMOTE_AUDIO_SHARING_FOR_SECOND_AUTO_CONNECTED_LEA_DEVICE})
public void displayPreference_flagOn_updateDeviceList() {
mController.displayPreference(mScreen);
}
@Test
public void onProfileConnectionStateChanged_notDisconnectedProfile_doNothing() {
CachedBluetoothDevice cachedDevice = mock(CachedBluetoothDevice.class);
mController.onProfileConnectionStateChanged(
cachedDevice, BluetoothAdapter.STATE_CONNECTED,
BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT);
verifyNoInteractions(mDialogHandler);
}
@Test
public void onProfileConnectionStateChanged_leaDeviceDisconnected_closeOpeningDialogsForIt() {
// Test when LEA device LE_AUDIO_BROADCAST_ASSISTANT disconnected.
BluetoothDevice device = mock(BluetoothDevice.class);
CachedBluetoothDevice cachedDevice = mock(CachedBluetoothDevice.class);
LeAudioProfile profile = mock(LeAudioProfile.class);
when(profile.isEnabled(device)).thenReturn(true);
when(cachedDevice.getProfiles()).thenReturn(ImmutableList.of(profile));
when(cachedDevice.isConnected()).thenReturn(true);
when(cachedDevice.getDevice()).thenReturn(device);
mController.onProfileConnectionStateChanged(
cachedDevice,
BluetoothAdapter.STATE_DISCONNECTED,
BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT);
verify(mDialogHandler).closeOpeningDialogsForLeaDevice(cachedDevice);
}
@Test
public void
onProfileConnectionStateChanged_classicDeviceDisconnected_closeOpeningDialogsForIt() {
// Test when classic device totally disconnected
BluetoothDevice device = mock(BluetoothDevice.class);
CachedBluetoothDevice cachedDevice = mock(CachedBluetoothDevice.class);
LeAudioProfile profile = mock(LeAudioProfile.class);
when(profile.isEnabled(device)).thenReturn(false);
when(cachedDevice.getProfiles()).thenReturn(ImmutableList.of(profile));
when(cachedDevice.isConnected()).thenReturn(false);
when(cachedDevice.getDevice()).thenReturn(device);
mController.onProfileConnectionStateChanged(
cachedDevice, BluetoothAdapter.STATE_DISCONNECTED, BluetoothProfile.A2DP);
verify(mDialogHandler).closeOpeningDialogsForNonLeaDevice(cachedDevice);
}
@Test
@EnableFlags({Flags.FLAG_ENABLE_LE_AUDIO_SHARING,
Flags.FLAG_PROMOTE_AUDIO_SHARING_FOR_SECOND_AUTO_CONNECTED_LEA_DEVICE})
public void handleDeviceConnectedFromIntent_noDevice_doNothing() {
Intent intent = new Intent();
doReturn(intent).when(mActivity).getIntent();
mController.displayPreference(mScreen);
shadowOf(Looper.getMainLooper()).idle();
verify(mDeviceManager, never()).findDevice(any(BluetoothDevice.class));
verify(mDialogHandler, never())
.handleDeviceConnected(any(CachedBluetoothDevice.class), anyBoolean());
}
@Test
@EnableFlags({Flags.FLAG_ENABLE_LE_AUDIO_SHARING,
Flags.FLAG_PROMOTE_AUDIO_SHARING_FOR_SECOND_AUTO_CONNECTED_LEA_DEVICE})
public void handleDeviceClickFromIntent_handle() {
CachedBluetoothDevice cachedDevice = mock(CachedBluetoothDevice.class);
BluetoothDevice device = mock(BluetoothDevice.class);
when(mDeviceManager.findDevice(device)).thenReturn(cachedDevice);
Intent intent = new Intent();
intent.putExtra(EXTRA_BLUETOOTH_DEVICE, device);
doReturn(intent).when(mActivity).getIntent();
mController.displayPreference(mScreen);
shadowOf(Looper.getMainLooper()).idle();
verify(mDialogHandler).handleDeviceConnected(cachedDevice, /* userTriggered = */ false);
}
@Test
public void testBluetoothLeBroadcastAssistantCallbacks_closeOpeningDialogsForSourceAdded() {
CachedBluetoothDevice cachedDevice = mock(CachedBluetoothDevice.class);
BluetoothDevice device = mock(BluetoothDevice.class);
when(mDeviceManager.findDevice(device)).thenReturn(cachedDevice);
// onSourceAdded will dismiss stale dialogs
mController.mAssistantCallback.onSourceAdded(device, /* sourceId= */
1, /* reason= */ 1);
verify(mDialogHandler).closeOpeningDialogsForLeaDevice(cachedDevice);
}
@Test
public void testBluetoothLeBroadcastAssistantCallbacks_doNothing() {
BluetoothDevice device = mock(BluetoothDevice.class);
mController.mAssistantCallback.onSearchStarted(/* reason= */ 1);
mController.mAssistantCallback.onSearchStartFailed(/* reason= */ 1);
mController.mAssistantCallback.onSearchStopped(/* reason= */ 1);
mController.mAssistantCallback.onSearchStopFailed(/* reason= */ 1);
BluetoothLeBroadcastReceiveState state = mock(BluetoothLeBroadcastReceiveState.class);
mController.mAssistantCallback.onReceiveStateChanged(device, /* sourceId= */ 1, state);
mController.mAssistantCallback.onSourceModified(device, /* sourceId= */ 1, /* reason= */ 1);
mController.mAssistantCallback.onSourceModifyFailed(device, /* sourceId= */ 1, /* reason= */
1);
BluetoothLeBroadcastMetadata metadata = mock(BluetoothLeBroadcastMetadata.class);
mController.mAssistantCallback.onSourceFound(metadata);
mController.mAssistantCallback.onSourceLost(/* broadcastId= */ 1);
shadowOf(Looper.getMainLooper()).idle();
// Above callbacks won't dismiss stale dialogs
verifyNoInteractions(mDialogHandler);
}
}

View File

@@ -0,0 +1,52 @@
/*
* 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.connecteddevice.audiosharing;
import static com.google.common.truth.Truth.assertThat;
import com.android.settings.R;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
@RunWith(RobolectricTestRunner.class)
public class AudioSharingJoinHandlerDashboardFragmentTest {
private AudioSharingJoinHandlerDashboardFragment mFragment;
@Before
public void setUp() {
mFragment = new AudioSharingJoinHandlerDashboardFragment();
}
@Test
public void getPreferenceScreenResId_returnsCorrectXml() {
assertThat(mFragment.getPreferenceScreenResId())
.isEqualTo(R.xml.bluetooth_le_audio_sharing_join_handler);
}
@Test
public void getLogTag_returnsCorrectTag() {
assertThat(mFragment.getLogTag()).isEqualTo("AudioSharingJoinHandlerFrag");
}
@Test
public void getMetricsCategory_returnsCorrectCategory() {
assertThat(mFragment.getMetricsCategory()).isEqualTo(0);
}
}