diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingIncompatibleDialogFragment.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingIncompatibleDialogFragment.java new file mode 100644 index 00000000000..5de615e968f --- /dev/null +++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingIncompatibleDialogFragment.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2024 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.app.Dialog; +import android.content.DialogInterface; +import android.os.Bundle; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; + +import com.android.settings.core.instrumentation.InstrumentedDialogFragment; +import com.android.settingslib.bluetooth.BluetoothUtils; +import com.android.settingslib.bluetooth.CachedBluetoothDevice; + +public class AudioSharingIncompatibleDialogFragment extends InstrumentedDialogFragment { + private static final String TAG = "AudioSharingIncompatDlg"; + + private static final String BUNDLE_KEY_DEVICE_NAME = "bundle_key_device_name"; + + // The host creates an instance of this dialog fragment must implement this interface to receive + // event callbacks. + public interface DialogEventListener { + /** + * Called when the dialog is dismissed. + */ + void onDialogDismissed(); + } + + @Nullable + private static DialogEventListener sListener; + + @Override + public int getMetricsCategory() { + // TODO: add metrics + return 0; + } + + /** + * Display the {@link AudioSharingIncompatibleDialogFragment} dialog. + * + * @param host The Fragment this dialog will be hosted. + */ + public static void show(@Nullable Fragment host, @NonNull CachedBluetoothDevice cachedDevice, + @NonNull DialogEventListener listener) { + if (host == null || !BluetoothUtils.isAudioSharingEnabled()) return; + final FragmentManager manager; + try { + manager = host.getChildFragmentManager(); + } catch (IllegalStateException e) { + Log.d(TAG, "Fail to show dialog: " + e.getMessage()); + return; + } + sListener = listener; + AlertDialog dialog = AudioSharingDialogHelper.getDialogIfShowing(manager, TAG); + if (dialog != null) { + Log.d(TAG, "Dialog is showing, return."); + return; + } + Log.d(TAG, "Show up the incompatible device dialog."); + final Bundle bundle = new Bundle(); + bundle.putString(BUNDLE_KEY_DEVICE_NAME, cachedDevice.getName()); + AudioSharingIncompatibleDialogFragment dialogFrag = + new AudioSharingIncompatibleDialogFragment(); + dialogFrag.setArguments(bundle); + dialogFrag.show(manager, TAG); + } + + @Override + @NonNull + public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + Bundle arguments = requireArguments(); + String deviceName = arguments.getString(BUNDLE_KEY_DEVICE_NAME); + // TODO: move strings to res once they are finalized + AlertDialog dialog = + AudioSharingDialogFactory.newBuilder(getActivity()) + .setTitle("Can't share audio with " + deviceName) + .setTitleIcon(com.android.settings.R.drawable.ic_warning_24dp) + .setIsCustomBodyEnabled(true) + .setCustomMessage( + "Audio sharing only works with headphones that support LE Audio.") + .setPositiveButton(com.android.settings.R.string.okay, (d, w) -> {}) + .build(); + dialog.setCanceledOnTouchOutside(true); + return dialog; + } + + @Override + public void onDismiss(@NonNull DialogInterface dialog) { + super.onDismiss(dialog); + if (sListener != null) { + sListener.onDialogDismissed(); + } + } +} diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingIncompatibleDialogFragmentTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingIncompatibleDialogFragmentTest.java new file mode 100644 index 00000000000..7f172918d36 --- /dev/null +++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingIncompatibleDialogFragmentTest.java @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2024 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.when; +import static org.robolectric.shadows.ShadowLooper.shadowMainLooper; + +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothStatusCodes; +import android.platform.test.flag.junit.SetFlagsRule; +import android.view.View; +import android.widget.TextView; + +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentActivity; + +import com.android.settings.R; +import com.android.settings.testutils.shadow.ShadowAlertDialogCompat; +import com.android.settings.testutils.shadow.ShadowBluetoothAdapter; +import com.android.settingslib.bluetooth.CachedBluetoothDevice; +import com.android.settingslib.flags.Flags; + +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.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.shadow.api.Shadow; +import org.robolectric.shadows.androidx.fragment.FragmentController; + +import java.util.concurrent.atomic.AtomicBoolean; + +@RunWith(RobolectricTestRunner.class) +@Config(shadows = {ShadowAlertDialogCompat.class, ShadowBluetoothAdapter.class}) +public class AudioSharingIncompatibleDialogFragmentTest { + private static final String TEST_DEVICE_NAME = "test"; + private static final AudioSharingIncompatibleDialogFragment.DialogEventListener + EMPTY_EVENT_LISTENER = () -> {}; + + @Rule public final MockitoRule mocks = MockitoJUnit.rule(); + @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + + @Mock private CachedBluetoothDevice mCachedBluetoothDevice; + private Fragment mParent; + private AudioSharingIncompatibleDialogFragment mFragment; + + @Before + public void setUp() { + ShadowAlertDialogCompat.reset(); + ShadowBluetoothAdapter shadowBluetoothAdapter = Shadow.extract( + BluetoothAdapter.getDefaultAdapter()); + shadowBluetoothAdapter.setEnabled(true); + shadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported( + BluetoothStatusCodes.FEATURE_SUPPORTED); + shadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported( + BluetoothStatusCodes.FEATURE_SUPPORTED); + when(mCachedBluetoothDevice.getName()).thenReturn(TEST_DEVICE_NAME); + mFragment = new AudioSharingIncompatibleDialogFragment(); + mParent = new Fragment(); + FragmentController.setupFragment(mParent, FragmentActivity.class, /* containerViewId= */ + 0, /* bundle= */ null); + } + + @After + public void tearDown() { + ShadowAlertDialogCompat.reset(); + } + + @Test + public void getMetricsCategory_correctValue() { + // TODO: update to real metrics id + assertThat(mFragment.getMetricsCategory()).isEqualTo(0); + } + + @Test + public void onCreateDialog_flagOff_dialogNotExist() { + mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + AudioSharingIncompatibleDialogFragment.show(mParent, mCachedBluetoothDevice, + EMPTY_EVENT_LISTENER); + shadowMainLooper().idle(); + AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog(); + assertThat(dialog).isNull(); + } + + @Test + public void onCreateDialog_unattachedFragment_dialogNotExist() { + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + AudioSharingIncompatibleDialogFragment.show(new Fragment(), mCachedBluetoothDevice, + EMPTY_EVENT_LISTENER); + shadowMainLooper().idle(); + AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog(); + assertThat(dialog).isNull(); + } + + @Test + public void onCreateDialog_flagOn_showDialog() { + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + AudioSharingIncompatibleDialogFragment.show(mParent, mCachedBluetoothDevice, + EMPTY_EVENT_LISTENER); + shadowMainLooper().idle(); + AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog(); + assertThat(dialog).isNotNull(); + assertThat(dialog.isShowing()).isTrue(); + TextView title = dialog.findViewById(R.id.title_text); + assertThat(title).isNotNull(); + // TODO: use string res + assertThat(title.getText().toString()).isEqualTo( + "Can't share audio with " + TEST_DEVICE_NAME); + } + + @Test + public void onCreateDialog_clickBtn_callbackTriggered() { + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + AtomicBoolean isBtnClicked = new AtomicBoolean(false); + AudioSharingIncompatibleDialogFragment.show(mParent, mCachedBluetoothDevice, + () -> isBtnClicked.set(true)); + shadowMainLooper().idle(); + AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog(); + assertThat(dialog).isNotNull(); + View btnView = dialog.findViewById(android.R.id.button1); + assertThat(btnView).isNotNull(); + btnView.performClick(); + shadowMainLooper().idle(); + assertThat(dialog.isShowing()).isFalse(); + assertThat(isBtnClicked.get()).isTrue(); + } +} + +