diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsDialogFragment.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsDialogFragment.java index 83b7d9a456a..afb0da747eb 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsDialogFragment.java +++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsDialogFragment.java @@ -68,7 +68,10 @@ public class AudioStreamsDialogFragment extends InstrumentedDialogFragment { * @param dialogBuilder The builder for constructing the dialog. * @param dialogId The dialog settings enum for logging */ - public static void show(Fragment host, DialogBuilder dialogBuilder, int dialogId) { + public static void show(@Nullable Fragment host, DialogBuilder dialogBuilder, int dialogId) { + if (host == null) { + return; + } if (!host.isAdded()) { Log.w(TAG, "The host fragment is not added to the activity!"); return; @@ -77,7 +80,10 @@ public class AudioStreamsDialogFragment extends InstrumentedDialogFragment { (new AudioStreamsDialogFragment(dialogBuilder, dialogId)).show(manager, TAG); } - static void dismissAll(Fragment host) { + static void dismissAll(@Nullable Fragment host) { + if (host == null) { + return; + } if (!host.isAdded()) { Log.w(TAG, "The host fragment is not added to the activity!"); return; diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryCallback.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryCallback.java index f0d0bebcddb..10dda373108 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryCallback.java +++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryCallback.java @@ -21,6 +21,7 @@ import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssista import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothLeBroadcastMetadata; import android.bluetooth.BluetoothLeBroadcastReceiveState; +import android.bluetooth.BluetoothStatusCodes; public class AudioStreamsProgressCategoryCallback extends AudioStreamsBroadcastAssistantCallback { private final AudioStreamsProgressCategoryController mCategoryController; @@ -44,6 +45,9 @@ public class AudioStreamsProgressCategoryCallback extends AudioStreamsBroadcastA @Override public void onSearchStartFailed(int reason) { + if (reason == BluetoothStatusCodes.ERROR_ALREADY_IN_TARGET_STATE) { + return; + } super.onSearchStartFailed(reason); mCategoryController.showToast("Failed to start scanning. Try again."); mCategoryController.setScanning(false); @@ -57,6 +61,9 @@ public class AudioStreamsProgressCategoryCallback extends AudioStreamsBroadcastA @Override public void onSearchStopFailed(int reason) { + if (reason == BluetoothStatusCodes.ERROR_ALREADY_IN_TARGET_STATE) { + return; + } super.onSearchStopFailed(reason); mCategoryController.showToast("Failed to stop scanning. Try again."); } diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java index 24978c6a753..e670c7e7f7a 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java +++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java @@ -16,6 +16,8 @@ package com.android.settings.connecteddevice.audiosharing.audiostreams; +import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsHelper.getEnabledScreenReaderServices; +import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsHelper.setAccessibilityServiceOff; import static com.android.settingslib.bluetooth.BluetoothUtils.isAudioSharingHysteresisModeFixAvailable; import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant.LocalBluetoothLeBroadcastSourceState.DECRYPTION_FAILED; import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant.LocalBluetoothLeBroadcastSourceState.PAUSED; @@ -32,8 +34,10 @@ import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothLeBroadcastMetadata; import android.bluetooth.BluetoothLeBroadcastReceiveState; import android.bluetooth.BluetoothProfile; +import android.content.ComponentName; import android.content.Context; import android.util.Log; +import android.view.accessibility.AccessibilityManager; import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; @@ -59,6 +63,7 @@ import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executor; import java.util.concurrent.Executors; @@ -103,6 +108,9 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro } }; + private final AccessibilityManager.AccessibilityServicesStateChangeListener + mAccessibilityListener = manager -> init(); + private final Comparator mComparator = Comparator.comparing( p -> @@ -148,6 +156,7 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro private SourceOriginForLogging mSourceFromQrCodeOriginForLogging; @Nullable private AudioStreamsProgressCategoryPreference mCategoryPreference; @Nullable private Fragment mFragment; + @Nullable AccessibilityManager mAccessibilityManager; public AudioStreamsProgressCategoryController(Context context, String preferenceKey) { super(context, preferenceKey); @@ -159,6 +168,7 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro mBroadcastAssistantCallback = new AudioStreamsProgressCategoryCallback(this); mHysteresisModeFixAvailable = BluetoothUtils.isAudioSharingHysteresisModeFixAvailable( mContext); + mAccessibilityManager = context.getSystemService(AccessibilityManager.class); } @Override @@ -177,6 +187,10 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro if (mBluetoothManager != null) { mBluetoothManager.getEventManager().registerCallback(mBluetoothCallback); } + if (mAccessibilityManager != null) { + mAccessibilityManager.addAccessibilityServicesStateChangeListener( + mExecutor, mAccessibilityListener); + } mExecutor.execute(this::init); } @@ -185,6 +199,10 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro if (mBluetoothManager != null) { mBluetoothManager.getEventManager().unregisterCallback(mBluetoothCallback); } + if (mAccessibilityManager != null) { + mAccessibilityManager.removeAccessibilityServicesStateChangeListener( + mAccessibilityListener); + } mExecutor.execute(this::stopScanning); } @@ -542,6 +560,7 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro boolean hasConnected = AudioStreamsHelper.getCachedBluetoothDeviceInSharingOrLeConnected(mBluetoothManager) .isPresent(); + Set screenReaderServices = getEnabledScreenReaderServices(mContext); AudioSharingUtils.postOnMainThread( mContext, () -> { @@ -550,27 +569,27 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro mCategoryPreference.setVisible(hasConnected); } }); - if (hasConnected) { + if (hasConnected && screenReaderServices.isEmpty()) { startScanning(); - AudioSharingUtils.postOnMainThread( - mContext, - () -> { - if (mFragment != null) { - AudioStreamsDialogFragment.dismissAll(mFragment); - } - }); + AudioSharingUtils.postOnMainThread(mContext, + () -> AudioStreamsDialogFragment.dismissAll(mFragment)); } else { stopScanning(); - AudioSharingUtils.postOnMainThread( - mContext, - () -> { - if (mFragment != null) { - AudioStreamsDialogFragment.show( - mFragment, - getNoLeDeviceDialog(), - SettingsEnums.DIALOG_AUDIO_STREAM_MAIN_NO_LE_DEVICE); - } - }); + if (!hasConnected) { + AudioSharingUtils.postOnMainThread( + mContext, () -> AudioStreamsDialogFragment.show( + mFragment, + getNoLeDeviceDialog(), + SettingsEnums.DIALOG_AUDIO_STREAM_MAIN_NO_LE_DEVICE) + ); + } else if (!screenReaderServices.isEmpty()) { + AudioSharingUtils.postOnMainThread( + mContext, () -> AudioStreamsDialogFragment.show( + mFragment, + getTurnOffTalkbackDialog(screenReaderServices), + SettingsEnums.DIALOG_AUDIO_STREAM_MAIN_TURN_OFF_TALKBACK) + ); + } } } @@ -601,6 +620,9 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro stateList.forEach( state -> handleSourcePaused(device, state))); } + if (DEBUG) { + Log.d(TAG, "startScanning()"); + } mLeBroadcastAssistant.startSearchingForSources(emptyList()); mMediaControlHelper.start(); }); @@ -735,4 +757,31 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro dialog.dismiss(); }); } + + private AudioStreamsDialogFragment.DialogBuilder getTurnOffTalkbackDialog( + Set enabledScreenReader) { + return new AudioStreamsDialogFragment.DialogBuilder(mContext) + .setTitle(mContext.getString(R.string.audio_streams_dialog_turn_off_talkback_title)) + .setSubTitle2(mContext.getString( + R.string.audio_streams_dialog_turn_off_talkback_subtitle)) + .setLeftButtonText(mContext.getString(R.string.cancel)) + .setLeftButtonOnClickListener(dialog -> { + dialog.dismiss(); + if (mFragment != null && mFragment.getActivity() != null) { + // Navigate back + mFragment.getActivity().finish(); + } + }) + .setRightButtonText( + mContext.getString(R.string.audio_streams_dialog_turn_off_talkback_button)) + .setRightButtonOnClickListener( + dialog -> { + ThreadUtils.postOnBackgroundThread(() -> { + if (!enabledScreenReader.isEmpty()) { + setAccessibilityServiceOff(mContext, enabledScreenReader); + } + }); + dialog.dismiss(); + }); + } } diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryCallbackTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryCallbackTest.java index 199284657e3..5cb7231b346 100644 --- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryCallbackTest.java +++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryCallbackTest.java @@ -22,6 +22,7 @@ import static com.android.settingslib.flags.Flags.FLAG_ENABLE_LE_AUDIO_SHARING; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -124,6 +125,15 @@ public class AudioStreamsProgressCategoryCallbackTest { verify(mController).setScanning(anyBoolean()); } + @Test + public void testOnSearchStartFailed_ignoreAlreadyInTargetState() { + mCallback.onSearchStartFailed(/* reason= */ + BluetoothStatusCodes.ERROR_ALREADY_IN_TARGET_STATE); + + verify(mController, never()).showToast(anyString()); + verify(mController, never()).setScanning(anyBoolean()); + } + @Test public void testOnSearchStarted() { mCallback.onSearchStarted(/* reason= */ 0); @@ -138,6 +148,14 @@ public class AudioStreamsProgressCategoryCallbackTest { verify(mController).showToast(anyString()); } + @Test + public void testOnSearchStopFailed_ignoreAlreadyInTargetState() { + mCallback.onSearchStopFailed(/* reason= */ + BluetoothStatusCodes.ERROR_ALREADY_IN_TARGET_STATE); + + verify(mController, never()).showToast(anyString()); + } + @Test public void testOnSearchStopped() { mCallback.onSearchStopped(/* reason= */ 0); diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryControllerTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryControllerTest.java index 8cccbd443f1..41e4be52116 100644 --- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryControllerTest.java +++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryControllerTest.java @@ -52,10 +52,12 @@ import android.bluetooth.BluetoothLeBroadcastMetadata; import android.bluetooth.BluetoothLeBroadcastReceiveState; import android.bluetooth.BluetoothProfile; import android.bluetooth.BluetoothStatusCodes; +import android.content.ComponentName; import android.content.Context; import android.os.Looper; import android.platform.test.flag.junit.SetFlagsRule; import android.view.View; +import android.view.accessibility.AccessibilityManager; import android.widget.Button; import android.widget.TextView; @@ -120,9 +122,11 @@ public class AudioStreamsProgressCategoryControllerTest { private static final String BROADCAST_NAME_1 = "name_1"; private static final String BROADCAST_NAME_2 = "name_2"; private static final byte[] BROADCAST_CODE = new byte[] {1}; - private final Context mContext = ApplicationProvider.getApplicationContext(); + private final Context mContext = spy(ApplicationProvider.getApplicationContext()); @Mock private LocalBluetoothManager mLocalBtManager; @Mock private BluetoothEventManager mBluetoothEventManager; + @Mock + private AccessibilityManager mAccessibilityManager; @Mock private PreferenceScreen mScreen; @Mock private AudioStreamsHelper mAudioStreamsHelper; @Mock private LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant; @@ -152,6 +156,8 @@ public class AudioStreamsProgressCategoryControllerTest { ShadowBluetoothUtils.sLocalBluetoothManager = mLocalBtManager; when(mLocalBtManager.getEventManager()).thenReturn(mBluetoothEventManager); when(mLeBroadcastAssistant.isSearchInProgress()).thenReturn(false); + when(mContext.getSystemService(AccessibilityManager.class)).thenReturn( + mAccessibilityManager); when(mScreen.findPreference(anyString())).thenReturn(mPreference); @@ -200,6 +206,7 @@ public class AudioStreamsProgressCategoryControllerTest { mController.onStop(mLifecycleOwner); verify(mBluetoothEventManager).unregisterCallback(any()); + verify(mAccessibilityManager).removeAccessibilityServicesStateChangeListener(any()); } @Test @@ -252,6 +259,56 @@ public class AudioStreamsProgressCategoryControllerTest { dialog.cancel(); } + @Test + public void testOnStart_initTalkbackOn_showDialog() { + // Setup a device + ShadowAudioStreamsHelper.setCachedBluetoothDeviceInSharingOrLeConnected(mDevice); + // Enable a screen reader service + ShadowAudioStreamsHelper.setEnabledScreenReaderService(new ComponentName("pkg", "class")); + when(mLeBroadcastAssistant.isSearchInProgress()).thenReturn(true); + + FragmentController.setupFragment(mFragment); + mController.setFragment(mFragment); + mController.displayPreference(mScreen); + mController.onStart(mLifecycleOwner); + shadowOf(Looper.getMainLooper()).idle(); + + // Called twice, once in displayPreference, the other in init() + verify(mPreference, times(2)).setVisible(anyBoolean()); + verify(mPreference).removeAudioStreamPreferences(); + verify(mLeBroadcastAssistant).stopSearchingForSources(); + verify(mLeBroadcastAssistant).unregisterServiceCallBack(any()); + + var dialog = ShadowAlertDialog.getLatestAlertDialog(); + assertThat(dialog).isNotNull(); + assertThat(dialog.isShowing()).isTrue(); + + TextView title = dialog.findViewById(R.id.dialog_title); + assertThat(title).isNotNull(); + assertThat(title.getText()) + .isEqualTo( + mContext.getString(R.string.audio_streams_dialog_turn_off_talkback_title)); + TextView subtitle1 = dialog.findViewById(R.id.dialog_subtitle); + assertThat(subtitle1).isNotNull(); + assertThat(subtitle1.getVisibility()).isEqualTo(View.GONE); + TextView subtitle2 = dialog.findViewById(R.id.dialog_subtitle_2); + assertThat(subtitle2).isNotNull(); + assertThat(subtitle2.getText()) + .isEqualTo(mContext.getString( + R.string.audio_streams_dialog_turn_off_talkback_subtitle)); + View leftButton = dialog.findViewById(R.id.left_button); + assertThat(leftButton).isNotNull(); + assertThat(leftButton.getVisibility()).isEqualTo(View.VISIBLE); + Button rightButton = dialog.findViewById(R.id.right_button); + assertThat(rightButton).isNotNull(); + assertThat(rightButton.getText()) + .isEqualTo( + mContext.getString(R.string.audio_streams_dialog_turn_off_talkback_button)); + assertThat(rightButton.hasOnClickListeners()).isTrue(); + + dialog.cancel(); + } + @Test public void testBluetoothOff_triggerRunnable() { mController.mBluetoothCallback.onBluetoothStateChanged(BluetoothAdapter.STATE_OFF);