diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingSwitchBarController.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingSwitchBarController.java index a8021326c41..8e091885213 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingSwitchBarController.java +++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingSwitchBarController.java @@ -16,6 +16,8 @@ package com.android.settings.connecteddevice.audiosharing; +import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast.EXTRA_START_LE_AUDIO_SHARING; + import android.app.settings.SettingsEnums; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; @@ -27,6 +29,7 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.os.Bundle; import android.util.FeatureFlagUtils; import android.util.Log; import android.util.Pair; @@ -44,6 +47,7 @@ import androidx.lifecycle.DefaultLifecycleObserver; import androidx.lifecycle.LifecycleOwner; import com.android.settings.R; +import com.android.settings.SettingsActivity; import com.android.settings.bluetooth.Utils; import com.android.settings.core.BasePreferenceController; import com.android.settings.overlay.FeatureFactory; @@ -66,11 +70,12 @@ import java.util.Map; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; public class AudioSharingSwitchBarController extends BasePreferenceController implements DefaultLifecycleObserver, - OnCheckedChangeListener, - LocalBluetoothProfileManager.ServiceListener { + OnCheckedChangeListener, + LocalBluetoothProfileManager.ServiceListener { private static final String TAG = "AudioSharingSwitchCtlr"; private static final String PREF_KEY = "audio_sharing_main_switch"; @@ -106,6 +111,8 @@ public class AudioSharingSwitchBarController extends BasePreferenceController private List mDeviceItemsForSharing = new ArrayList<>(); @VisibleForTesting IntentFilter mIntentFilter; private final AtomicBoolean mCallbacksRegistered = new AtomicBoolean(false); + private AtomicInteger mIntentHandleStage = + new AtomicInteger(StartIntentHandleStage.TO_HANDLE.ordinal()); @VisibleForTesting BroadcastReceiver mReceiver = @@ -309,6 +316,12 @@ public class AudioSharingSwitchBarController extends BasePreferenceController return; } registerCallbacks(); + if (mIntentHandleStage.compareAndSet( + StartIntentHandleStage.TO_HANDLE.ordinal(), + StartIntentHandleStage.HANDLE_AUTO_ADD.ordinal())) { + Log.d(TAG, "onStart: handleStartAudioSharingFromIntent"); + handleStartAudioSharingFromIntent(); + } } @Override @@ -344,8 +357,8 @@ public class AudioSharingSwitchBarController extends BasePreferenceController // FeatureFlagUtils.SETTINGS_NEED_CONNECTED_BLE_DEVICE_FOR_BROADCAST is always true in // prod. We can turn off the flag for debug purpose. if (FeatureFlagUtils.isEnabled( - mContext, - FeatureFlagUtils.SETTINGS_NEED_CONNECTED_BLE_DEVICE_FOR_BROADCAST) + mContext, + FeatureFlagUtils.SETTINGS_NEED_CONNECTED_BLE_DEVICE_FOR_BROADCAST) && mAssistant.getAllConnectedDevices().isEmpty()) { // Pop up dialog to ask users to connect at least one lea buds before audio sharing. AudioSharingUtils.postOnMainThread( @@ -386,6 +399,12 @@ public class AudioSharingSwitchBarController extends BasePreferenceController if (mProfileManager != null) { mProfileManager.removeServiceListener(this); } + if (mIntentHandleStage.compareAndSet( + StartIntentHandleStage.TO_HANDLE.ordinal(), + StartIntentHandleStage.HANDLE_AUTO_ADD.ordinal())) { + Log.d(TAG, "onServiceConnected: handleStartAudioSharingFromIntent"); + handleStartAudioSharingFromIntent(); + } } } @@ -489,7 +508,7 @@ public class AudioSharingSwitchBarController extends BasePreferenceController boolean isStateReady = isBluetoothOn() && AudioSharingUtils.isAudioSharingProfileReady( - mProfileManager); + mProfileManager); AudioSharingUtils.postOnMainThread( mContext, () -> { @@ -526,7 +545,24 @@ public class AudioSharingSwitchBarController extends BasePreferenceController AudioSharingUtils.addSourceToTargetSinks(mTargetActiveSinks, mBtManager); mMetricsFeatureProvider.action(mContext, SettingsEnums.ACTION_AUTO_JOIN_AUDIO_SHARING); mTargetActiveSinks.clear(); + if (mIntentHandleStage.compareAndSet( + StartIntentHandleStage.HANDLE_AUTO_ADD.ordinal(), + StartIntentHandleStage.HANDLED.ordinal()) + && mDeviceItemsForSharing.size() == 1) { + Log.d(TAG, "handleOnBroadcastReady: auto add source to the second device"); + AudioSharingUtils.addSourceToTargetSinks( + mGroupedConnectedDevices.getOrDefault( + mDeviceItemsForSharing.get(0).getGroupId(), ImmutableList.of()), + mBtManager); + mGroupedConnectedDevices.clear(); + mDeviceItemsForSharing.clear(); + // TODO: Add metric for auto add by intent + return; + } } + mIntentHandleStage.compareAndSet( + StartIntentHandleStage.HANDLE_AUTO_ADD.ordinal(), + StartIntentHandleStage.HANDLED.ordinal()); if (mFragment == null) { Log.d(TAG, "handleOnBroadcastReady: dialog fail to show due to null fragment."); mGroupedConnectedDevices.clear(); @@ -572,12 +608,58 @@ public class AudioSharingSwitchBarController extends BasePreferenceController @NonNull ViewGroup host, @NonNull View view, @NonNull AccessibilityEvent event) { if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED && (event.getContentChangeTypes() - & AccessibilityEvent.CONTENT_CHANGE_TYPE_ENABLED) - != 0) { + & AccessibilityEvent.CONTENT_CHANGE_TYPE_ENABLED) + != 0) { Log.d(TAG, "Skip accessibility event for CONTENT_CHANGE_TYPE_ENABLED"); return false; } return super.onRequestSendAccessibilityEvent(host, view, event); } } + + private void handleStartAudioSharingFromIntent() { + var unused = + ThreadUtils.postOnBackgroundThread( + () -> { + if (mFragment == null + || mFragment.getActivity() == null + || mFragment.getActivity().getIntent() == null) { + Log.d( + TAG, + "Skip handleStartAudioSharingFromIntent, " + + "fragment intent is null"); + return; + } + Intent intent = mFragment.getActivity().getIntent(); + Bundle args = + intent.getBundleExtra( + SettingsActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS); + Boolean shouldStart = + args != null + && args.getBoolean(EXTRA_START_LE_AUDIO_SHARING, false); + if (!shouldStart) { + Log.d(TAG, "Skip handleStartAudioSharingFromIntent, arg false"); + mIntentHandleStage.compareAndSet( + StartIntentHandleStage.HANDLE_AUTO_ADD.ordinal(), + StartIntentHandleStage.HANDLED.ordinal()); + return; + } + if (BluetoothUtils.isBroadcasting(mBtManager)) { + Log.d(TAG, "Skip handleStartAudioSharingFromIntent, in broadcast"); + mIntentHandleStage.compareAndSet( + StartIntentHandleStage.HANDLE_AUTO_ADD.ordinal(), + StartIntentHandleStage.HANDLED.ordinal()); + return; + } + Log.d(TAG, "HandleStartAudioSharingFromIntent, start broadcast"); + AudioSharingUtils.postOnMainThread( + mContext, () -> mSwitchBar.setChecked(true)); + }); + } + + private enum StartIntentHandleStage { + TO_HANDLE, + HANDLE_AUTO_ADD, + HANDLED, + } } diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingSwitchBarControllerTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingSwitchBarControllerTest.java index 711ef6f6955..558bc10bace 100644 --- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingSwitchBarControllerTest.java +++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingSwitchBarControllerTest.java @@ -18,6 +18,7 @@ package com.android.settings.connecteddevice.audiosharing; import static com.android.settings.core.BasePreferenceController.AVAILABLE; import static com.android.settings.core.BasePreferenceController.UNSUPPORTED_ON_DEVICE; +import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast.EXTRA_START_LE_AUDIO_SHARING; import static com.google.common.truth.Truth.assertThat; @@ -33,6 +34,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; import static org.robolectric.Shadows.shadowOf; +import static org.robolectric.shadows.ShadowLooper.shadowMainLooper; import android.app.settings.SettingsEnums; import android.bluetooth.BluetoothAdapter; @@ -47,6 +49,7 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.os.Bundle; import android.os.Looper; import android.platform.test.flag.junit.SetFlagsRule; import android.util.FeatureFlagUtils; @@ -55,14 +58,18 @@ import android.view.View; import android.view.accessibility.AccessibilityEvent; import android.widget.CompoundButton; +import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.DialogFragment; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; import androidx.lifecycle.LifecycleOwner; import androidx.test.core.app.ApplicationProvider; +import com.android.settings.R; +import com.android.settings.SettingsActivity; import com.android.settings.bluetooth.Utils; import com.android.settings.testutils.FakeFeatureFactory; +import com.android.settings.testutils.shadow.ShadowAlertDialogCompat; import com.android.settings.testutils.shadow.ShadowBluetoothAdapter; import com.android.settings.testutils.shadow.ShadowBluetoothUtils; import com.android.settings.testutils.shadow.ShadowThreadUtils; @@ -105,6 +112,7 @@ import java.util.concurrent.Executor; ShadowBluetoothAdapter.class, ShadowBluetoothUtils.class, ShadowThreadUtils.class, + ShadowAlertDialogCompat.class }) public class AudioSharingSwitchBarControllerTest { private static final String TEST_DEVICE_NAME1 = "test1"; @@ -129,6 +137,7 @@ public class AudioSharingSwitchBarControllerTest { @Mock private LocalBluetoothLeBroadcast mBroadcast; @Mock private LocalBluetoothLeBroadcastAssistant mAssistant; @Mock private VolumeControlProfile mVolumeControl; + @Mock private BluetoothLeBroadcastMetadata mMetadata; @Mock private CompoundButton mBtnView; @Mock private CachedBluetoothDevice mCachedDevice1; @Mock private CachedBluetoothDevice mCachedDevice2; @@ -434,6 +443,7 @@ public class AudioSharingSwitchBarControllerTest { mContext, FeatureFlagUtils.SETTINGS_NEED_CONNECTED_BLE_DEVICE_FOR_BROADCAST, true); when(mBtnView.isEnabled()).thenReturn(true); when(mAssistant.getAllConnectedDevices()).thenReturn(ImmutableList.of(mDevice2, mDevice1)); + when(mBroadcast.getLatestBluetoothLeBroadcastMetadata()).thenReturn(mMetadata); doNothing().when(mBroadcast).startPrivateBroadcast(); mController = new AudioSharingSwitchBarController( @@ -466,6 +476,7 @@ public class AudioSharingSwitchBarControllerTest { when(mAssistant.getAllConnectedDevices()).thenReturn(ImmutableList.of(mDevice2, mDevice1)); doNothing().when(mBroadcast).startPrivateBroadcast(); mController.onCheckedChanged(mBtnView, /* isChecked= */ true); + when(mBroadcast.getLatestBluetoothLeBroadcastMetadata()).thenReturn(mMetadata); verify(mBroadcast).startPrivateBroadcast(); mController.mBroadcastCallback.onPlaybackStarted(0, 0); shadowOf(Looper.getMainLooper()).idle(); @@ -502,6 +513,58 @@ public class AudioSharingSwitchBarControllerTest { 1)); } + @Test + public void onPlaybackStarted_clickShareBtnOnDialog_addSource() { + FeatureFlagUtils.setEnabled( + mContext, FeatureFlagUtils.SETTINGS_NEED_CONNECTED_BLE_DEVICE_FOR_BROADCAST, true); + when(mBtnView.isEnabled()).thenReturn(true); + when(mAssistant.getAllConnectedDevices()).thenReturn(ImmutableList.of(mDevice2, mDevice1)); + when(mBroadcast.getLatestBluetoothLeBroadcastMetadata()).thenReturn(mMetadata); + doNothing().when(mBroadcast).startPrivateBroadcast(); + mController.onCheckedChanged(mBtnView, /* isChecked= */ true); + verify(mBroadcast).startPrivateBroadcast(); + mController.mBroadcastCallback.onPlaybackStarted(0, 0); + shadowOf(Looper.getMainLooper()).idle(); + + verify(mAssistant).addSource(mDevice2, mMetadata, /* isGroupOp= */ false); + + AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog(); + assertThat(dialog).isNotNull(); + View btnView = dialog.findViewById(R.id.positive_btn); + assertThat(btnView).isNotNull(); + btnView.performClick(); + shadowMainLooper().idle(); + + verify(mAssistant).addSource(mDevice1, mMetadata, /* isGroupOp= */ false); + assertThat(dialog.isShowing()).isFalse(); + } + + @Test + public void onPlaybackStarted_clickCancelBtnOnDialog_doNothing() { + FeatureFlagUtils.setEnabled( + mContext, FeatureFlagUtils.SETTINGS_NEED_CONNECTED_BLE_DEVICE_FOR_BROADCAST, true); + when(mBtnView.isEnabled()).thenReturn(true); + when(mAssistant.getAllConnectedDevices()).thenReturn(ImmutableList.of(mDevice2, mDevice1)); + when(mBroadcast.getLatestBluetoothLeBroadcastMetadata()).thenReturn(mMetadata); + doNothing().when(mBroadcast).startPrivateBroadcast(); + mController.onCheckedChanged(mBtnView, /* isChecked= */ true); + verify(mBroadcast).startPrivateBroadcast(); + mController.mBroadcastCallback.onPlaybackStarted(0, 0); + shadowOf(Looper.getMainLooper()).idle(); + + verify(mAssistant).addSource(mDevice2, mMetadata, /* isGroupOp= */ false); + + AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog(); + assertThat(dialog).isNotNull(); + View btnView = dialog.findViewById(R.id.negative_btn); + assertThat(btnView).isNotNull(); + btnView.performClick(); + shadowMainLooper().idle(); + + verify(mAssistant, never()).addSource(mDevice1, mMetadata, /* isGroupOp= */ false); + assertThat(dialog.isShowing()).isFalse(); + } + @Test public void testBluetoothLeBroadcastCallbacks_updateSwitch() { mOnAudioSharingStateChanged = false; @@ -543,8 +606,7 @@ public class AudioSharingSwitchBarControllerTest { @Test public void testBluetoothLeBroadcastCallbacks_doNothing() { - BluetoothLeBroadcastMetadata metadata = mock(BluetoothLeBroadcastMetadata.class); - mController.mBroadcastCallback.onBroadcastMetadataChanged(/* reason= */ 1, metadata); + mController.mBroadcastCallback.onBroadcastMetadataChanged(/* reason= */ 1, mMetadata); mController.mBroadcastCallback.onBroadcastUpdated(/* reason= */ 1, /* broadcastId= */ 1); mController.mBroadcastCallback.onPlaybackStarted(/* reason= */ 1, /* broadcastId= */ 1); mController.mBroadcastCallback.onPlaybackStopped(/* reason= */ 1, /* broadcastId= */ 1); @@ -556,9 +618,8 @@ public class AudioSharingSwitchBarControllerTest { @Test public void testBluetoothLeBroadcastAssistantCallbacks_logAction() { - BluetoothLeBroadcastMetadata metadata = mock(BluetoothLeBroadcastMetadata.class); mController.mBroadcastAssistantCallback.onSourceAddFailed( - mDevice1, metadata, /* reason= */ 1); + mDevice1, mMetadata, /* reason= */ 1); verify(mFeatureFactory.metricsFeatureProvider) .action( mContext, @@ -569,7 +630,6 @@ public class AudioSharingSwitchBarControllerTest { @Test public void testBluetoothLeBroadcastAssistantCallbacks_doNothing() { BluetoothLeBroadcastReceiveState state = mock(BluetoothLeBroadcastReceiveState.class); - BluetoothLeBroadcastMetadata metadata = mock(BluetoothLeBroadcastMetadata.class); // Do nothing mController.mBroadcastAssistantCallback.onReceiveStateChanged( @@ -588,7 +648,7 @@ public class AudioSharingSwitchBarControllerTest { mDevice1, /* sourceId= */ 1, /* reason= */ 1); mController.mBroadcastAssistantCallback.onSourceModifyFailed( mDevice1, /* sourceId= */ 1, /* reason= */ 1); - mController.mBroadcastAssistantCallback.onSourceFound(metadata); + mController.mBroadcastAssistantCallback.onSourceFound(mMetadata); mController.mBroadcastAssistantCallback.onSourceLost(/* broadcastId= */ 1); verifyNoMoreInteractions(mFeatureFactory.metricsFeatureProvider); } @@ -614,4 +674,74 @@ public class AudioSharingSwitchBarControllerTest { .onRequestSendAccessibilityEvent(mSwitchBar, view, event)) .isFalse(); } + + @Test + public void handleStartAudioSharingFromIntent_flagOff_doNothing() { + mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + setUpStartSharingIntent(); + mController.onStart(mLifecycleOwner); + shadowOf(Looper.getMainLooper()).idle(); + + verify(mSwitchBar, never()).setChecked(true); + } + + @Test + public void handleStartAudioSharingFromIntent_profileNotReady_doNothing() { + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + when(mAssistant.isProfileReady()).thenReturn(false); + setUpStartSharingIntent(); + mController.onServiceConnected(); + shadowOf(Looper.getMainLooper()).idle(); + + verify(mSwitchBar, never()).setChecked(true); + } + + @Test + public void handleStartAudioSharingFromIntent_argFalse_doNothing() { + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + mController.onStart(mLifecycleOwner); + shadowOf(Looper.getMainLooper()).idle(); + + verify(mSwitchBar, never()).setChecked(true); + } + + @Test + public void handleStartAudioSharingFromIntent_handle() { + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + when(mBtnView.isEnabled()).thenReturn(true); + when(mAssistant.getAllConnectedDevices()).thenReturn(ImmutableList.of(mDevice2, mDevice1)); + when(mBroadcast.getLatestBluetoothLeBroadcastMetadata()).thenReturn(mMetadata); + setUpStartSharingIntent(); + mController.onServiceConnected(); + shadowOf(Looper.getMainLooper()).idle(); + + verify(mSwitchBar).setChecked(true); + doNothing().when(mBroadcast).startPrivateBroadcast(); + mController.onCheckedChanged(mBtnView, /* isChecked= */ true); + mController.mBroadcastCallback.onPlaybackStarted(0, 0); + shadowOf(Looper.getMainLooper()).idle(); + + verify(mFeatureFactory.metricsFeatureProvider) + .action(any(Context.class), eq(SettingsEnums.ACTION_AUTO_JOIN_AUDIO_SHARING)); + verify(mAssistant).addSource(mDevice1, mMetadata, /* isGroupOp= */ false); + verify(mAssistant).addSource(mDevice2, mMetadata, /* isGroupOp= */ false); + List childFragments = mParentFragment.getChildFragmentManager().getFragments(); + assertThat(childFragments).isEmpty(); + } + + private void setUpStartSharingIntent() { + Bundle args = new Bundle(); + args.putBoolean(EXTRA_START_LE_AUDIO_SHARING, true); + Intent intent = new Intent(); + intent.putExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS, args); + Fragment fragment = new Fragment(); + FragmentController.of(fragment, intent) + .create(/* containerViewId= */ 0, /* bundle= */ null) + .start() + .resume() + .visible() + .get(); + shadowOf(Looper.getMainLooper()).idle(); + mController.init(fragment); + } }