diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNamePreference.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNamePreference.java index 0bb6b607041..bfccdc4c672 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNamePreference.java +++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNamePreference.java @@ -95,14 +95,14 @@ public class AudioSharingNamePreference extends ValidatedEditTextPreference { } private void configureInvisibleStateForQrCodeIcon(ImageButton shareButton, View divider) { - divider.setVisibility(View.INVISIBLE); - shareButton.setVisibility(View.INVISIBLE); + divider.setVisibility(View.GONE); + shareButton.setVisibility(View.GONE); shareButton.setOnClickListener(null); } private void launchAudioSharingQrCodeFragment() { new SubSettingLauncher(getContext()) - .setTitleText(getContext().getString(R.string.audio_streams_qr_code_page_title)) + .setTitleRes(R.string.audio_streams_qr_code_page_title) .setDestination(AudioStreamsQrCodeFragment.class.getName()) .setSourceMetricsCategory(SettingsEnums.AUDIO_SHARING_SETTINGS) .launch(); diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNamePreferenceController.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNamePreferenceController.java index 24b8f20cf51..894ba487014 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNamePreferenceController.java +++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNamePreferenceController.java @@ -26,6 +26,7 @@ import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import androidx.lifecycle.DefaultLifecycleObserver; import androidx.lifecycle.LifecycleOwner; import androidx.preference.Preference; @@ -56,7 +57,8 @@ public class AudioSharingNamePreferenceController extends BasePreferenceControll private static final boolean DEBUG = BluetoothUtils.D; private static final String PREF_KEY = "audio_sharing_stream_name"; - private final BluetoothLeBroadcast.Callback mBroadcastCallback = + @VisibleForTesting + final BluetoothLeBroadcast.Callback mBroadcastCallback = new BluetoothLeBroadcast.Callback() { @Override public void onBroadcastMetadataChanged( diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPasswordPreferenceController.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPasswordPreferenceController.java index 258cf3be843..14930e11a6a 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPasswordPreferenceController.java +++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPasswordPreferenceController.java @@ -19,12 +19,19 @@ package com.android.settings.connecteddevice.audiosharing; import static com.android.settings.connecteddevice.audiosharing.AudioSharingUtils.isBroadcasting; import android.app.settings.SettingsEnums; +import android.content.ContentResolver; import android.content.Context; import android.content.SharedPreferences; +import android.database.ContentObserver; +import android.os.Handler; +import android.os.Looper; +import android.provider.Settings; import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.lifecycle.DefaultLifecycleObserver; +import androidx.lifecycle.LifecycleOwner; import androidx.preference.PreferenceScreen; import com.android.settings.R; @@ -41,15 +48,19 @@ import java.nio.charset.StandardCharsets; public class AudioSharingPasswordPreferenceController extends BasePreferenceController implements ValidatedEditTextPreference.Validator, - AudioSharingPasswordPreference.OnDialogEventListener { - + AudioSharingPasswordPreference.OnDialogEventListener, + DefaultLifecycleObserver { private static final String TAG = "AudioSharingPasswordPreferenceController"; private static final String PREF_KEY = "audio_sharing_stream_password"; private static final String SHARED_PREF_NAME = "audio_sharing_settings"; private static final String SHARED_PREF_KEY = "default_password"; + @Nullable private final ContentResolver mContentResolver; + @Nullable private final SharedPreferences mSharedPref; @Nullable private final LocalBluetoothManager mBtManager; @Nullable private final LocalBluetoothLeBroadcast mBroadcast; @Nullable private AudioSharingPasswordPreference mPreference; + private final ContentObserver mSettingsObserver; + private final SharedPreferences.OnSharedPreferenceChangeListener mSharedPrefChangeListener; private final AudioSharingPasswordValidator mAudioSharingPasswordValidator; private final MetricsFeatureProvider mMetricsFeatureProvider; @@ -61,9 +72,44 @@ public class AudioSharingPasswordPreferenceController extends BasePreferenceCont ? mBtManager.getProfileManager().getLeAudioBroadcastProfile() : null; mAudioSharingPasswordValidator = new AudioSharingPasswordValidator(); + mContentResolver = context.getContentResolver(); + mSettingsObserver = new PasswordSettingsObserver(); + mSharedPref = context.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE); + mSharedPrefChangeListener = new PasswordSharedPrefChangeListener(); mMetricsFeatureProvider = FeatureFactory.getFeatureFactory().getMetricsFeatureProvider(); } + @Override + public void onStart(@NonNull LifecycleOwner owner) { + if (!isAvailable()) { + Log.d(TAG, "Feature is not available."); + return; + } + if (mContentResolver != null) { + mContentResolver.registerContentObserver( + Settings.Secure.getUriFor(Settings.Secure.BLUETOOTH_LE_BROADCAST_CODE), + false, + mSettingsObserver); + } + if (mSharedPref != null) { + mSharedPref.registerOnSharedPreferenceChangeListener(mSharedPrefChangeListener); + } + } + + @Override + public void onStop(@NonNull LifecycleOwner owner) { + if (!isAvailable()) { + Log.d(TAG, "Feature is not available."); + return; + } + if (mContentResolver != null) { + mContentResolver.unregisterContentObserver(mSettingsObserver); + } + if (mSharedPref != null) { + mSharedPref.unregisterOnSharedPreferenceChangeListener(mSharedPrefChangeListener); + } + } + @Override public int getAvailabilityStatus() { return AudioSharingUtils.isFeatureEnabled() ? AVAILABLE : UNSUPPORTED_ON_DEVICE; @@ -125,7 +171,6 @@ public class AudioSharingPasswordPreferenceController extends BasePreferenceCont persistDefaultPassword(mContext, password); mBroadcast.setBroadcastCode( isPublicBroadcast ? new byte[0] : password.getBytes()); - updatePreference(); mMetricsFeatureProvider.action( mContext, SettingsEnums.ACTION_AUDIO_STREAM_PASSWORD_UPDATED, @@ -164,32 +209,52 @@ public class AudioSharingPasswordPreferenceController extends BasePreferenceCont }); } - private static void persistDefaultPassword(Context context, String defaultPassword) { + private class PasswordSettingsObserver extends ContentObserver { + PasswordSettingsObserver() { + super(new Handler(Looper.getMainLooper())); + } + + @Override + public void onChange(boolean selfChange) { + Log.d(TAG, "onChange, broadcast password has been changed"); + updatePreference(); + } + } + + private class PasswordSharedPrefChangeListener + implements SharedPreferences.OnSharedPreferenceChangeListener { + @Override + public void onSharedPreferenceChanged( + SharedPreferences sharedPreferences, @Nullable String key) { + if (!SHARED_PREF_KEY.equals(key)) { + return; + } + Log.d(TAG, "onSharedPreferenceChanged, default password has been changed"); + updatePreference(); + } + } + + private void persistDefaultPassword(Context context, String defaultPassword) { if (getDefaultPassword(context).equals(defaultPassword)) { return; } - - SharedPreferences sharedPref = - context.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE); - if (sharedPref == null) { + if (mSharedPref == null) { Log.w(TAG, "persistDefaultPassword(): sharedPref is empty!"); return; } - SharedPreferences.Editor editor = sharedPref.edit(); + SharedPreferences.Editor editor = mSharedPref.edit(); editor.putString(SHARED_PREF_KEY, defaultPassword); editor.apply(); } - private static String getDefaultPassword(Context context) { - SharedPreferences sharedPref = - context.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE); - if (sharedPref == null) { + private String getDefaultPassword(Context context) { + if (mSharedPref == null) { Log.w(TAG, "getDefaultPassword(): sharedPref is empty!"); return ""; } - String value = sharedPref.getString(SHARED_PREF_KEY, ""); + String value = mSharedPref.getString(SHARED_PREF_KEY, ""); if (value != null && value.isEmpty()) { Log.w(TAG, "getDefaultPassword(): default password is empty!"); } diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNamePreferenceControllerTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNamePreferenceControllerTest.java new file mode 100644 index 00000000000..618e02129e6 --- /dev/null +++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNamePreferenceControllerTest.java @@ -0,0 +1,319 @@ +/* + * 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.android.settings.core.BasePreferenceController.AVAILABLE; +import static com.android.settings.core.BasePreferenceController.UNSUPPORTED_ON_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.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.settings.SettingsEnums; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothLeBroadcast; +import android.bluetooth.BluetoothLeBroadcastMetadata; +import android.bluetooth.BluetoothStatusCodes; +import android.content.Context; +import android.platform.test.flag.junit.SetFlagsRule; + +import androidx.lifecycle.LifecycleOwner; +import androidx.preference.PreferenceScreen; +import androidx.test.core.app.ApplicationProvider; + +import com.android.settings.bluetooth.Utils; +import com.android.settings.testutils.FakeFeatureFactory; +import com.android.settings.testutils.shadow.ShadowBluetoothAdapter; +import com.android.settings.testutils.shadow.ShadowBluetoothUtils; +import com.android.settingslib.bluetooth.BluetoothEventManager; +import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast; +import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant; +import com.android.settingslib.bluetooth.LocalBluetoothManager; +import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; +import com.android.settingslib.bluetooth.VolumeControlProfile; +import com.android.settingslib.core.lifecycle.Lifecycle; +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.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 org.robolectric.shadows.ShadowLooper; + +import java.util.concurrent.Executor; + +@RunWith(RobolectricTestRunner.class) +@Config( + shadows = { + ShadowBluetoothAdapter.class, + ShadowBluetoothUtils.class, + }) +public class AudioSharingNamePreferenceControllerTest { + private static final String PREF_KEY = "audio_sharing_stream_name"; + private static final String BROADCAST_NAME = "broadcast_name"; + private static final CharSequence UPDATED_NAME = "updated_name"; + + @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule(); + @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + @Spy Context mContext = ApplicationProvider.getApplicationContext(); + @Mock private LocalBluetoothLeBroadcast mBroadcast; + @Mock private LocalBluetoothLeBroadcastAssistant mAssistant; + @Mock private VolumeControlProfile mVolumeControl; + @Mock private LocalBluetoothManager mLocalBtManager; + @Mock private BluetoothEventManager mEventManager; + @Mock private LocalBluetoothProfileManager mProfileManager; + @Mock private PreferenceScreen mScreen; + private AudioSharingNamePreferenceController mController; + private ShadowBluetoothAdapter mShadowBluetoothAdapter; + private Lifecycle mLifecycle; + private LifecycleOwner mLifecycleOwner; + private AudioSharingNamePreference mPreference; + private FakeFeatureFactory mFeatureFactory; + + @Before + public void setUp() { + mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter()); + mShadowBluetoothAdapter.setEnabled(true); + mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported( + BluetoothStatusCodes.FEATURE_SUPPORTED); + mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported( + BluetoothStatusCodes.FEATURE_SUPPORTED); + ShadowBluetoothUtils.sLocalBluetoothManager = mLocalBtManager; + mLocalBtManager = Utils.getLocalBtManager(mContext); + when(mLocalBtManager.getEventManager()).thenReturn(mEventManager); + when(mLocalBtManager.getProfileManager()).thenReturn(mProfileManager); + when(mProfileManager.getLeAudioBroadcastProfile()).thenReturn(mBroadcast); + when(mProfileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(mAssistant); + when(mProfileManager.getVolumeControlProfile()).thenReturn(mVolumeControl); + when(mBroadcast.isProfileReady()).thenReturn(true); + when(mAssistant.isProfileReady()).thenReturn(true); + when(mVolumeControl.isProfileReady()).thenReturn(true); + when(mBroadcast.isProfileReady()).thenReturn(true); + mFeatureFactory = FakeFeatureFactory.setupForTest(); + mLifecycleOwner = () -> mLifecycle; + mLifecycle = new Lifecycle(mLifecycleOwner); + mController = new AudioSharingNamePreferenceController(mContext, PREF_KEY); + mPreference = spy(new AudioSharingNamePreference(mContext)); + when(mScreen.findPreference(PREF_KEY)).thenReturn(mPreference); + } + + @Test + public void getAvailabilityStatus_flagOn_available() { + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE); + } + + @Test + public void getAvailabilityStatus_flagOff_unsupported() { + mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + assertThat(mController.getAvailabilityStatus()).isEqualTo(UNSUPPORTED_ON_DEVICE); + } + + @Test + public void onStart_flagOff_doNothing() { + mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + mController.onStart(mLifecycleOwner); + verify(mBroadcast, never()) + .registerServiceCallBack( + any(Executor.class), any(BluetoothLeBroadcast.Callback.class)); + } + + @Test + public void onStart_flagOn_registerCallbacks() { + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + mController.onStart(mLifecycleOwner); + verify(mBroadcast) + .registerServiceCallBack( + any(Executor.class), any(BluetoothLeBroadcast.Callback.class)); + } + + @Test + public void onStart_flagOn_serviceNotReady_registerCallbacks() { + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + when(mBroadcast.isProfileReady()).thenReturn(false); + mController.onStart(mLifecycleOwner); + verify(mProfileManager) + .addServiceListener(any(LocalBluetoothProfileManager.ServiceListener.class)); + } + + @Test + public void onServiceConnected_removeCallbacks() { + mController.onServiceConnected(); + verify(mProfileManager) + .removeServiceListener(any(LocalBluetoothProfileManager.ServiceListener.class)); + } + + @Test + public void onStop_flagOff_doNothing() { + mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + mController.onStart(mLifecycleOwner); + mController.onStop(mLifecycleOwner); + verify(mBroadcast, never()) + .unregisterServiceCallBack(any(BluetoothLeBroadcast.Callback.class)); + } + + @Test + public void onStop_flagOn_unregisterCallbacks() { + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + mController.onStart(mLifecycleOwner); + mController.onStop(mLifecycleOwner); + verify(mBroadcast).unregisterServiceCallBack(any(BluetoothLeBroadcast.Callback.class)); + } + + @Test + public void displayPreference_updateName_showIcon() { + when(mBroadcast.getBroadcastName()).thenReturn(BROADCAST_NAME); + when(mBroadcast.isEnabled(any())).thenReturn(true); + mController.displayPreference(mScreen); + ShadowLooper.idleMainLooper(); + + assertThat(mPreference.getText()).isEqualTo(BROADCAST_NAME); + assertThat(mPreference.getSummary()).isEqualTo(BROADCAST_NAME); + verify(mPreference).setValidator(any()); + verify(mPreference).setShowQrCodeIcon(true); + } + + @Test + public void displayPreference_updateName_hideIcon() { + when(mBroadcast.getBroadcastName()).thenReturn(BROADCAST_NAME); + when(mBroadcast.isEnabled(any())).thenReturn(false); + mController.displayPreference(mScreen); + ShadowLooper.idleMainLooper(); + + assertThat(mPreference.getText()).isEqualTo(BROADCAST_NAME); + assertThat(mPreference.getSummary()).isEqualTo(BROADCAST_NAME); + verify(mPreference).setValidator(any()); + verify(mPreference).setShowQrCodeIcon(false); + } + + @Test + public void onPreferenceChange_noChange_doNothing() { + when(mPreference.getSummary()).thenReturn(BROADCAST_NAME); + mController.displayPreference(mScreen); + boolean changed = mController.onPreferenceChange(mPreference, BROADCAST_NAME); + ShadowLooper.idleMainLooper(); + + verify(mBroadcast, never()).setBroadcastName(anyString()); + verify(mBroadcast, never()).setProgramInfo(anyString()); + verify(mBroadcast, never()).updateBroadcast(); + verify(mFeatureFactory.metricsFeatureProvider, never()).action(any(), anyInt(), anyInt()); + + assertThat(changed).isFalse(); + } + + @Test + public void onPreferenceChange_changed_updateName_broadcasting() { + when(mPreference.getSummary()).thenReturn(BROADCAST_NAME); + when(mBroadcast.isEnabled(any())).thenReturn(true); + mController.displayPreference(mScreen); + boolean changed = mController.onPreferenceChange(mPreference, UPDATED_NAME); + ShadowLooper.idleMainLooper(); + + verify(mBroadcast).setBroadcastName(UPDATED_NAME.toString()); + verify(mBroadcast).setProgramInfo(UPDATED_NAME.toString()); + verify(mBroadcast).updateBroadcast(); + verify(mFeatureFactory.metricsFeatureProvider) + .action(mContext, SettingsEnums.ACTION_AUDIO_STREAM_NAME_UPDATED, 1); + assertThat(changed).isTrue(); + } + + @Test + public void onPreferenceChange_changed_updateName_notBroadcasting() { + when(mPreference.getSummary()).thenReturn(BROADCAST_NAME); + when(mBroadcast.isEnabled(any())).thenReturn(false); + mController.displayPreference(mScreen); + boolean changed = mController.onPreferenceChange(mPreference, UPDATED_NAME); + ShadowLooper.idleMainLooper(); + + verify(mBroadcast).setBroadcastName(UPDATED_NAME.toString()); + verify(mBroadcast).setProgramInfo(UPDATED_NAME.toString()); + verify(mBroadcast, never()).updateBroadcast(); + verify(mFeatureFactory.metricsFeatureProvider) + .action(mContext, SettingsEnums.ACTION_AUDIO_STREAM_NAME_UPDATED, 0); + assertThat(changed).isTrue(); + } + + @Test + public void unrelatedCallbacks_doNotUpdateIcon() { + mController.displayPreference(mScreen); + mController.mBroadcastCallback.onBroadcastStartFailed(/* reason= */ 0); + mController.mBroadcastCallback.onBroadcastStarted(/* reason= */ 0, /* broadcastId= */ 0); + mController.mBroadcastCallback.onBroadcastStopFailed(/* reason= */ 0); + mController.mBroadcastCallback.onBroadcastUpdateFailed( + /* reason= */ 0, /* broadcastId= */ 0); + mController.mBroadcastCallback.onBroadcastUpdated(/* reason= */ 0, /* broadcastId= */ 0); + mController.mBroadcastCallback.onPlaybackStarted(/* reason= */ 0, /* broadcastId= */ 0); + mController.mBroadcastCallback.onPlaybackStopped(/* reason= */ 0, /* broadcastId= */ 0); + + ShadowLooper.idleMainLooper(); + // Should be called once in displayPreference, but not called after callbacks + verify(mPreference).setShowQrCodeIcon(anyBoolean()); + } + + @Test + public void broadcastOnCallback_updateIcon() { + mController.displayPreference(mScreen); + mController.mBroadcastCallback.onBroadcastMetadataChanged( + /* broadcastId= */ 0, mock(BluetoothLeBroadcastMetadata.class)); + + ShadowLooper.idleMainLooper(); + + // Should be called twice, in displayPreference and also after callback + verify(mPreference, times(2)).setShowQrCodeIcon(anyBoolean()); + } + + @Test + public void broadcastStopCallback_updateIcon() { + mController.displayPreference(mScreen); + mController.mBroadcastCallback.onBroadcastStopped(/* reason= */ 0, /* broadcastId= */ 0); + + ShadowLooper.idleMainLooper(); + + // Should be called twice, in displayPreference and also after callback + verify(mPreference, times(2)).setShowQrCodeIcon(anyBoolean()); + } + + @Test + public void idTextValid_emptyString() { + boolean valid = mController.isTextValid(""); + + assertThat(valid).isFalse(); + } + + @Test + public void idTextValid_validName() { + boolean valid = mController.isTextValid("valid name"); + + assertThat(valid).isTrue(); + } +} diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNamePreferenceTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNamePreferenceTest.java new file mode 100644 index 00000000000..13e2a9d4636 --- /dev/null +++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNamePreferenceTest.java @@ -0,0 +1,141 @@ +/* + * 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.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.content.Intent; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.ImageButton; +import android.widget.LinearLayout; + +import androidx.preference.PreferenceViewHolder; +import androidx.test.core.app.ApplicationProvider; + +import com.android.settings.R; +import com.android.settings.SettingsActivity; +import com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsQrCodeFragment; +import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class AudioSharingNamePreferenceTest { + @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule(); + private Context mContext; + private AudioSharingNamePreference mPreference; + + @Before + public void setup() { + mContext = ApplicationProvider.getApplicationContext(); + mPreference = spy(new AudioSharingNamePreference(mContext, null)); + } + + @Test + public void initialize_correctLayout() { + assertThat(mPreference.getLayoutResource()) + .isEqualTo( + com.android.settingslib.widget.preference.twotarget.R.layout + .preference_two_target); + assertThat(mPreference.getWidgetLayoutResource()) + .isEqualTo(R.layout.preference_widget_qrcode); + } + + @Test + public void onBindViewHolder_correctLayout_noQrCodeButton() { + LayoutInflater inflater = LayoutInflater.from(mContext); + View view = inflater.inflate(mPreference.getLayoutResource(), null); + LinearLayout widgetView = view.findViewById(android.R.id.widget_frame); + assertThat(widgetView).isNotNull(); + inflater.inflate(mPreference.getWidgetLayoutResource(), widgetView, true); + + var holder = PreferenceViewHolder.createInstanceForTests(view); + mPreference.setShowQrCodeIcon(false); + mPreference.onBindViewHolder(holder); + + ImageButton shareButton = (ImageButton) holder.findViewById(R.id.button_icon); + View divider = + holder.findViewById( + com.android.settingslib.widget.preference.twotarget.R.id + .two_target_divider); + + assertThat(shareButton).isNotNull(); + assertThat(shareButton.getVisibility()).isEqualTo(View.GONE); + assertThat(shareButton.hasOnClickListeners()).isFalse(); + assertThat(divider).isNotNull(); + assertThat(divider.getVisibility()).isEqualTo(View.GONE); + } + + @Test + public void onBindViewHolder_correctLayout_showQrCodeButton() { + LayoutInflater inflater = LayoutInflater.from(mContext); + View view = inflater.inflate(mPreference.getLayoutResource(), null); + LinearLayout widgetView = view.findViewById(android.R.id.widget_frame); + assertThat(widgetView).isNotNull(); + inflater.inflate(mPreference.getWidgetLayoutResource(), widgetView, true); + + var holder = PreferenceViewHolder.createInstanceForTests(view); + mPreference.setShowQrCodeIcon(true); + mPreference.onBindViewHolder(holder); + + ImageButton shareButton = (ImageButton) holder.findViewById(R.id.button_icon); + View divider = + holder.findViewById( + com.android.settingslib.widget.preference.twotarget.R.id + .two_target_divider); + + assertThat(shareButton).isNotNull(); + assertThat(shareButton.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(shareButton.getDrawable()).isNotNull(); + assertThat(shareButton.hasOnClickListeners()).isTrue(); + assertThat(divider).isNotNull(); + assertThat(divider.getVisibility()).isEqualTo(View.VISIBLE); + + // mContext is not an Activity context, calling startActivity() from outside of an Activity + // context requires the FLAG_ACTIVITY_NEW_TASK flag, create a mock to avoid this + // AndroidRuntimeException. + Context activityContext = mock(Context.class); + when(mPreference.getContext()).thenReturn(activityContext); + shareButton.callOnClick(); + + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Intent.class); + verify(activityContext).startActivity(argumentCaptor.capture()); + + Intent intent = argumentCaptor.getValue(); + assertThat(intent.getStringExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT)) + .isEqualTo(AudioStreamsQrCodeFragment.class.getName()); + assertThat(intent.getIntExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_TITLE_RESID, 0)) + .isEqualTo(R.string.audio_streams_qr_code_page_title); + assertThat(intent.getIntExtra(MetricsFeatureProvider.EXTRA_SOURCE_METRICS_CATEGORY, 0)) + .isEqualTo(SettingsEnums.AUDIO_SHARING_SETTINGS); + } +} diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNameTextValidatorTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNameTextValidatorTest.java new file mode 100644 index 00000000000..ada6117b34a --- /dev/null +++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNameTextValidatorTest.java @@ -0,0 +1,52 @@ +/* + * 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 org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class AudioSharingNameTextValidatorTest { + private AudioSharingNameTextValidator mValidator; + + @Before + public void setUp() { + mValidator = new AudioSharingNameTextValidator(); + } + + @Test + public void testValidNames() { + assertThat(mValidator.isTextValid("ValidName")).isTrue(); + assertThat(mValidator.isTextValid("12345678")).isTrue(); + assertThat(mValidator.isTextValid("Name_With_Underscores")).isTrue(); + assertThat(mValidator.isTextValid("ÄÖÜß")).isTrue(); + assertThat(mValidator.isTextValid("ThisNameIsExactly32Characters!")).isTrue(); + } + + @Test + public void testInvalidNames() { + assertThat(mValidator.isTextValid(null)).isFalse(); + assertThat(mValidator.isTextValid("")).isFalse(); + assertThat(mValidator.isTextValid("abc")).isFalse(); + assertThat(mValidator.isTextValid("ThisNameIsWayTooLongForAnAudioSharingName")).isFalse(); + assertThat(mValidator.isTextValid("Invalid\uDC00")).isFalse(); + } +} diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPasswordPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPasswordPreferenceControllerTest.java new file mode 100644 index 00000000000..5bfb9663e11 --- /dev/null +++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPasswordPreferenceControllerTest.java @@ -0,0 +1,335 @@ +/* + * 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.android.settings.core.BasePreferenceController.AVAILABLE; +import static com.android.settings.core.BasePreferenceController.UNSUPPORTED_ON_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.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.settings.SettingsEnums; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothStatusCodes; +import android.content.ContentResolver; +import android.content.Context; +import android.content.SharedPreferences; +import android.database.ContentObserver; +import android.platform.test.flag.junit.SetFlagsRule; + +import androidx.lifecycle.LifecycleOwner; +import androidx.preference.PreferenceScreen; +import androidx.test.core.app.ApplicationProvider; + +import com.android.settings.R; +import com.android.settings.bluetooth.Utils; +import com.android.settings.testutils.FakeFeatureFactory; +import com.android.settings.testutils.shadow.ShadowBluetoothAdapter; +import com.android.settings.testutils.shadow.ShadowBluetoothUtils; +import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast; +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 org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +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 org.robolectric.shadows.ShadowLooper; + +import java.nio.charset.StandardCharsets; + +@RunWith(RobolectricTestRunner.class) +@Config( + shadows = { + ShadowBluetoothAdapter.class, + ShadowBluetoothUtils.class, + }) +public class AudioSharingPasswordPreferenceControllerTest { + private static final String PREF_KEY = "audio_sharing_stream_password"; + private static final String SHARED_PREF_KEY = "default_password"; + private static final String BROADCAST_PASSWORD = "password"; + private static final String EDITTEXT_PASSWORD = "edittext_password"; + private static final String HIDDEN_PASSWORD = "********"; + + @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule(); + @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + @Spy Context mContext = ApplicationProvider.getApplicationContext(); + @Mock private LocalBluetoothLeBroadcast mBroadcast; + @Mock private LocalBluetoothManager mLocalBtManager; + @Mock private LocalBluetoothProfileManager mProfileManager; + @Mock private SharedPreferences mSharedPreferences; + @Mock private SharedPreferences.Editor mEditor; + @Mock private ContentResolver mContentResolver; + @Mock private PreferenceScreen mScreen; + private AudioSharingPasswordPreferenceController mController; + private ShadowBluetoothAdapter mShadowBluetoothAdapter; + private Lifecycle mLifecycle; + private LifecycleOwner mLifecycleOwner; + private AudioSharingPasswordPreference mPreference; + private FakeFeatureFactory mFeatureFactory; + + @Before + public void setUp() { + mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter()); + mShadowBluetoothAdapter.setEnabled(true); + mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported( + BluetoothStatusCodes.FEATURE_SUPPORTED); + mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported( + BluetoothStatusCodes.FEATURE_SUPPORTED); + mLocalBtManager = Utils.getLocalBtManager(mContext); + when(mLocalBtManager.getProfileManager()).thenReturn(mProfileManager); + when(mProfileManager.getLeAudioBroadcastProfile()).thenReturn(mBroadcast); + mFeatureFactory = FakeFeatureFactory.setupForTest(); + when(mContext.getContentResolver()).thenReturn(mContentResolver); + when(mContext.getSharedPreferences(anyString(), anyInt())).thenReturn(mSharedPreferences); + when(mSharedPreferences.edit()).thenReturn(mEditor); + when(mEditor.putString(anyString(), anyString())).thenReturn(mEditor); + mLifecycleOwner = () -> mLifecycle; + mLifecycle = new Lifecycle(mLifecycleOwner); + mController = new AudioSharingPasswordPreferenceController(mContext, PREF_KEY); + mPreference = spy(new AudioSharingPasswordPreference(mContext)); + when(mScreen.findPreference(PREF_KEY)).thenReturn(mPreference); + } + + @Test + public void getAvailabilityStatus_flagOn_available() { + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE); + } + + @Test + public void getAvailabilityStatus_flagOff_unsupported() { + mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + assertThat(mController.getAvailabilityStatus()).isEqualTo(UNSUPPORTED_ON_DEVICE); + } + + @Test + public void onStart_flagOff_doNothing() { + mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + mController.onStart(mLifecycleOwner); + verify(mContentResolver, never()).registerContentObserver(any(), anyBoolean(), any()); + verify(mSharedPreferences, never()).registerOnSharedPreferenceChangeListener(any()); + } + + @Test + public void onStart_flagOn_registerCallbacks() { + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + mController.onStart(mLifecycleOwner); + verify(mContentResolver).registerContentObserver(any(), anyBoolean(), any()); + verify(mSharedPreferences).registerOnSharedPreferenceChangeListener(any()); + } + + @Test + public void onStop_flagOff_doNothing() { + mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + mController.onStop(mLifecycleOwner); + verify(mContentResolver, never()).unregisterContentObserver(any()); + verify(mSharedPreferences, never()).unregisterOnSharedPreferenceChangeListener(any()); + } + + @Test + public void onStop_flagOn_registerCallbacks() { + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + mController.onStop(mLifecycleOwner); + verify(mContentResolver).unregisterContentObserver(any()); + verify(mSharedPreferences).unregisterOnSharedPreferenceChangeListener(any()); + } + + @Test + public void displayPreference_setupPreference_noPassword() { + when(mSharedPreferences.getString(anyString(), anyString())).thenReturn(EDITTEXT_PASSWORD); + when(mBroadcast.getBroadcastCode()).thenReturn(new byte[] {}); + + mController.displayPreference(mScreen); + ShadowLooper.idleMainLooper(); + + assertThat(mPreference.isPassword()).isTrue(); + assertThat(mPreference.getDialogLayoutResource()) + .isEqualTo(R.layout.audio_sharing_password_dialog); + assertThat(mPreference.getText()).isEqualTo(EDITTEXT_PASSWORD); + assertThat(mPreference.getSummary()) + .isEqualTo(mContext.getString(R.string.audio_streams_no_password_summary)); + verify(mPreference).setValidator(any()); + verify(mPreference).setOnDialogEventListener(any()); + } + + @Test + public void contentObserver_updatePreferenceOnChange() { + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + when(mBroadcast.getBroadcastCode()) + .thenReturn(BROADCAST_PASSWORD.getBytes(StandardCharsets.UTF_8)); + mController.onStart(mLifecycleOwner); + mController.displayPreference(mScreen); + ShadowLooper.idleMainLooper(); + + ArgumentCaptor observerCaptor = + ArgumentCaptor.forClass(ContentObserver.class); + verify(mContentResolver) + .registerContentObserver(any(), anyBoolean(), observerCaptor.capture()); + + var observer = observerCaptor.getValue(); + assertThat(observer).isNotNull(); + observer.onChange(true); + verify(mPreference).setText(anyString()); + verify(mPreference).setSummary(anyString()); + } + + @Test + public void sharedPrefChangeListener_updatePreferenceOnChange() { + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + when(mBroadcast.getBroadcastCode()) + .thenReturn(BROADCAST_PASSWORD.getBytes(StandardCharsets.UTF_8)); + mController.onStart(mLifecycleOwner); + mController.displayPreference(mScreen); + ShadowLooper.idleMainLooper(); + + ArgumentCaptor captor = + ArgumentCaptor.forClass(SharedPreferences.OnSharedPreferenceChangeListener.class); + verify(mSharedPreferences).registerOnSharedPreferenceChangeListener(captor.capture()); + + var observer = captor.getValue(); + assertThat(captor).isNotNull(); + observer.onSharedPreferenceChanged(mSharedPreferences, SHARED_PREF_KEY); + verify(mPreference).setText(anyString()); + verify(mPreference).setSummary(anyString()); + } + + @Test + public void displayPreference_setupPreference_hasPassword() { + when(mBroadcast.getBroadcastCode()) + .thenReturn(BROADCAST_PASSWORD.getBytes(StandardCharsets.UTF_8)); + mController.displayPreference(mScreen); + ShadowLooper.idleMainLooper(); + + assertThat(mPreference.isPassword()).isTrue(); + assertThat(mPreference.getDialogLayoutResource()) + .isEqualTo(R.layout.audio_sharing_password_dialog); + assertThat(mPreference.getText()).isEqualTo(BROADCAST_PASSWORD); + assertThat(mPreference.getSummary()).isEqualTo(HIDDEN_PASSWORD); + verify(mPreference).setValidator(any()); + verify(mPreference).setOnDialogEventListener(any()); + } + + @Test + public void onBindDialogView_updatePreference_isBroadcasting_noPassword() { + when(mBroadcast.getBroadcastCode()).thenReturn(new byte[] {}); + when(mBroadcast.isEnabled(any())).thenReturn(true); + mController.displayPreference(mScreen); + mController.onBindDialogView(); + ShadowLooper.idleMainLooper(); + + verify(mPreference).setEditable(false); + verify(mPreference).setChecked(true); + } + + @Test + public void onBindDialogView_updatePreference_isNotBroadcasting_hasPassword() { + when(mBroadcast.getBroadcastCode()) + .thenReturn(BROADCAST_PASSWORD.getBytes(StandardCharsets.UTF_8)); + mController.displayPreference(mScreen); + mController.onBindDialogView(); + ShadowLooper.idleMainLooper(); + + verify(mPreference).setEditable(true); + verify(mPreference).setChecked(false); + } + + @Test + public void onPreferenceDataChanged_isBroadcasting_doNothing() { + when(mBroadcast.isEnabled(any())).thenReturn(true); + mController.displayPreference(mScreen); + mController.onPreferenceDataChanged(BROADCAST_PASSWORD, /* isPublicBroadcast= */ false); + ShadowLooper.idleMainLooper(); + + verify(mBroadcast, never()).setBroadcastCode(any()); + verify(mFeatureFactory.metricsFeatureProvider, never()).action(any(), anyInt(), anyInt()); + } + + @Test + public void onPreferenceDataChanged_noChange_doNothing() { + when(mSharedPreferences.getString(anyString(), anyString())).thenReturn(EDITTEXT_PASSWORD); + when(mBroadcast.getBroadcastCode()).thenReturn(new byte[] {}); + mController.displayPreference(mScreen); + mController.onPreferenceDataChanged(EDITTEXT_PASSWORD, /* isPublicBroadcast= */ true); + ShadowLooper.idleMainLooper(); + + verify(mBroadcast, never()).setBroadcastCode(any()); + verify(mFeatureFactory.metricsFeatureProvider, never()).action(any(), anyInt(), anyInt()); + } + + @Test + public void onPreferenceDataChanged_updateToNonPublicBroadcast() { + when(mSharedPreferences.getString(anyString(), anyString())).thenReturn(EDITTEXT_PASSWORD); + when(mBroadcast.getBroadcastCode()).thenReturn(new byte[] {}); + mController.displayPreference(mScreen); + mController.onPreferenceDataChanged(BROADCAST_PASSWORD, /* isPublicBroadcast= */ false); + ShadowLooper.idleMainLooper(); + + verify(mBroadcast).setBroadcastCode(BROADCAST_PASSWORD.getBytes(StandardCharsets.UTF_8)); + verify(mEditor).putString(anyString(), eq(BROADCAST_PASSWORD)); + verify(mFeatureFactory.metricsFeatureProvider) + .action(mContext, SettingsEnums.ACTION_AUDIO_STREAM_PASSWORD_UPDATED, 0); + } + + @Test + public void onPreferenceDataChanged_updateToPublicBroadcast() { + when(mSharedPreferences.getString(anyString(), anyString())).thenReturn(EDITTEXT_PASSWORD); + when(mBroadcast.getBroadcastCode()) + .thenReturn(BROADCAST_PASSWORD.getBytes(StandardCharsets.UTF_8)); + mController.displayPreference(mScreen); + mController.onPreferenceDataChanged(EDITTEXT_PASSWORD, /* isPublicBroadcast= */ true); + ShadowLooper.idleMainLooper(); + + verify(mBroadcast).setBroadcastCode("".getBytes(StandardCharsets.UTF_8)); + verify(mEditor, never()).putString(anyString(), eq(EDITTEXT_PASSWORD)); + verify(mFeatureFactory.metricsFeatureProvider) + .action(mContext, SettingsEnums.ACTION_AUDIO_STREAM_PASSWORD_UPDATED, 1); + } + + @Test + public void idTextValid_emptyString() { + boolean valid = mController.isTextValid(""); + + assertThat(valid).isFalse(); + } + + @Test + public void idTextValid_validPassword() { + boolean valid = mController.isTextValid(BROADCAST_PASSWORD); + + assertThat(valid).isTrue(); + } +} diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPasswordPreferenceTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPasswordPreferenceTest.java new file mode 100644 index 00000000000..0b87e8ca25d --- /dev/null +++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPasswordPreferenceTest.java @@ -0,0 +1,215 @@ +/* + * 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 android.view.View.GONE; +import static android.view.View.VISIBLE; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +import android.content.Context; +import android.content.DialogInterface; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.CheckBox; +import android.widget.EditText; + +import androidx.appcompat.app.AlertDialog; +import androidx.test.core.app.ApplicationProvider; + +import com.android.settings.R; + +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.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class AudioSharingPasswordPreferenceTest { + @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule(); + private static final String EDIT_TEXT_CONTENT = "text"; + private Context mContext; + private AudioSharingPasswordPreference mPreference; + + @Before + public void setup() { + mContext = ApplicationProvider.getApplicationContext(); + mPreference = new AudioSharingPasswordPreference(mContext, null); + } + + @Test + public void onBindDialogView_correctLayout() { + View view = + LayoutInflater.from(mContext).inflate(R.layout.audio_sharing_password_dialog, null); + mPreference.onBindDialogView(view); + + var editText = view.findViewById(android.R.id.edit); + var checkBox = view.findViewById(R.id.audio_sharing_stream_password_checkbox); + var dialogMessage = view.findViewById(android.R.id.message); + + assertThat(editText).isNotNull(); + assertThat(checkBox).isNotNull(); + assertThat(dialogMessage).isNotNull(); + } + + @Test + public void setEditable_true() { + View view = + LayoutInflater.from(mContext).inflate(R.layout.audio_sharing_password_dialog, null); + mPreference.onBindDialogView(view); + + var editText = view.findViewById(android.R.id.edit); + var checkBox = view.findViewById(R.id.audio_sharing_stream_password_checkbox); + var dialogMessage = view.findViewById(android.R.id.message); + + mPreference.setEditable(true); + + assertThat(editText).isNotNull(); + assertThat(editText.isEnabled()).isTrue(); + assertThat(editText.getAlpha()).isEqualTo(1.0f); + assertThat(checkBox).isNotNull(); + assertThat(checkBox.isEnabled()).isTrue(); + assertThat(dialogMessage).isNotNull(); + assertThat(dialogMessage.getVisibility()).isEqualTo(GONE); + } + + @Test + public void setEditable_false() { + View view = + LayoutInflater.from(mContext).inflate(R.layout.audio_sharing_password_dialog, null); + mPreference.onBindDialogView(view); + + var editText = view.findViewById(android.R.id.edit); + var checkBox = view.findViewById(R.id.audio_sharing_stream_password_checkbox); + var dialogMessage = view.findViewById(android.R.id.message); + + mPreference.setEditable(false); + + assertThat(editText).isNotNull(); + assertThat(editText.isEnabled()).isFalse(); + assertThat(editText.getAlpha()).isLessThan(1.0f); + assertThat(checkBox).isNotNull(); + assertThat(checkBox.isEnabled()).isFalse(); + assertThat(dialogMessage).isNotNull(); + assertThat(dialogMessage.getVisibility()).isEqualTo(VISIBLE); + } + + @Test + public void setChecked_true() { + View view = + LayoutInflater.from(mContext).inflate(R.layout.audio_sharing_password_dialog, null); + mPreference.onBindDialogView(view); + + CheckBox checkBox = view.findViewById(R.id.audio_sharing_stream_password_checkbox); + + mPreference.setChecked(true); + + assertThat(checkBox).isNotNull(); + assertThat(checkBox.isChecked()).isTrue(); + } + + @Test + public void setChecked_false() { + View view = + LayoutInflater.from(mContext).inflate(R.layout.audio_sharing_password_dialog, null); + mPreference.onBindDialogView(view); + + CheckBox checkBox = view.findViewById(R.id.audio_sharing_stream_password_checkbox); + + mPreference.setChecked(false); + + assertThat(checkBox).isNotNull(); + assertThat(checkBox.isChecked()).isFalse(); + } + + @Test + public void onDialogEventListener_onClick_positiveButton() { + AudioSharingPasswordPreference.OnDialogEventListener listener = + mock(AudioSharingPasswordPreference.OnDialogEventListener.class); + mPreference.setOnDialogEventListener(listener); + View view = + LayoutInflater.from(mContext).inflate(R.layout.audio_sharing_password_dialog, null); + mPreference.onBindDialogView(view); + + EditText editText = view.findViewById(android.R.id.edit); + assertThat(editText).isNotNull(); + editText.setText(EDIT_TEXT_CONTENT); + + mPreference.onClick(mock(DialogInterface.class), DialogInterface.BUTTON_POSITIVE); + + verify(listener).onBindDialogView(); + verify(listener).onPreferenceDataChanged(eq(EDIT_TEXT_CONTENT), anyBoolean()); + } + + @Test + public void onDialogEventListener_onClick_negativeButton_doNothing() { + AudioSharingPasswordPreference.OnDialogEventListener listener = + mock(AudioSharingPasswordPreference.OnDialogEventListener.class); + mPreference.setOnDialogEventListener(listener); + View view = + LayoutInflater.from(mContext).inflate(R.layout.audio_sharing_password_dialog, null); + mPreference.onBindDialogView(view); + + EditText editText = view.findViewById(android.R.id.edit); + assertThat(editText).isNotNull(); + editText.setText(EDIT_TEXT_CONTENT); + + mPreference.onClick(mock(DialogInterface.class), DialogInterface.BUTTON_NEGATIVE); + + verify(listener).onBindDialogView(); + verify(listener, never()).onPreferenceDataChanged(anyString(), anyBoolean()); + } + + @Test + public void onPrepareDialogBuilder_editable_doNothing() { + View view = + LayoutInflater.from(mContext).inflate(R.layout.audio_sharing_password_dialog, null); + mPreference.onBindDialogView(view); + mPreference.setEditable(true); + + var dialogBuilder = mock(AlertDialog.Builder.class); + mPreference.onPrepareDialogBuilder( + dialogBuilder, mock(DialogInterface.OnClickListener.class)); + + verify(dialogBuilder, never()).setPositiveButton(any(), any()); + } + + @Test + public void onPrepareDialogBuilder_notEditable_disableButton() { + View view = + LayoutInflater.from(mContext).inflate(R.layout.audio_sharing_password_dialog, null); + mPreference.onBindDialogView(view); + mPreference.setEditable(false); + + var dialogBuilder = mock(AlertDialog.Builder.class); + mPreference.onPrepareDialogBuilder( + dialogBuilder, mock(DialogInterface.OnClickListener.class)); + + verify(dialogBuilder).setPositiveButton(any(), any()); + } +} diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPasswordValidatorTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPasswordValidatorTest.java new file mode 100644 index 00000000000..5c96fe1b5bf --- /dev/null +++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPasswordValidatorTest.java @@ -0,0 +1,53 @@ +/* + * 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 org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class AudioSharingPasswordValidatorTest { + private AudioSharingPasswordValidator mValidator; + + @Before + public void setUp() { + mValidator = new AudioSharingPasswordValidator(); + } + + @Test + public void testValidPasswords() { + assertThat(mValidator.isTextValid("1234")).isTrue(); + assertThat(mValidator.isTextValid("Password")).isTrue(); + assertThat(mValidator.isTextValid("SecurePass123!")).isTrue(); + assertThat(mValidator.isTextValid("ÄÖÜß")).isTrue(); + assertThat(mValidator.isTextValid("1234567890abcdef")).isTrue(); + } + + @Test + public void testInvalidPasswords() { + assertThat(mValidator.isTextValid(null)).isFalse(); + assertThat(mValidator.isTextValid("")).isFalse(); + assertThat(mValidator.isTextValid("abc")).isFalse(); + assertThat(mValidator.isTextValid("ThisIsAVeryLongPasswordThatExceedsSixteenOctets")) + .isFalse(); + assertThat(mValidator.isTextValid("Invalid\uDC00")).isFalse(); + } +}