diff --git a/res/xml/bluetooth_audio_streams_qr_code.xml b/res/xml/bluetooth_audio_streams_qr_code.xml index a098845ad3c..5ec5505c4bb 100644 --- a/res/xml/bluetooth_audio_streams_qr_code.xml +++ b/res/xml/bluetooth_audio_streams_qr_code.xml @@ -47,8 +47,7 @@ + android:layout_height="@dimen/qrcode_size"/> { + BluetoothLeBroadcastMetadata broadcastMetadata = getBroadcastMetadata(); + if (broadcastMetadata == null) { + return; + } + Bitmap bm = getQrCodeBitmap(broadcastMetadata).orElse(null); + if (bm == null) { + return; + } - if (broadcastMetadata != null) { - Optional bm = getQrCodeBitmap(broadcastMetadata); - if (bm.isEmpty()) { - return view; - } - ((ImageView) view.requireViewById(R.id.qrcode_view)).setImageBitmap(bm.get()); - if (broadcastMetadata.getBroadcastCode() != null) { - String password = - new String(broadcastMetadata.getBroadcastCode(), StandardCharsets.UTF_8); - String passwordText = - getContext() - .getString(R.string.audio_streams_qr_code_page_password, password); - ((TextView) view.requireViewById(R.id.password)).setText(passwordText); - } - TextView summaryView = view.requireViewById(android.R.id.summary); - String summary = - view.getContext() - .getString( - R.string.audio_streams_qr_code_page_description, - broadcastMetadata.getBroadcastName()); - summaryView.setText(summary); - } - return view; + ThreadUtils.postOnMainThread( + () -> { + ((ImageView) view.requireViewById(R.id.qrcode_view)) + .setImageBitmap(bm); + if (broadcastMetadata.getBroadcastCode() != null) { + String password = + new String( + broadcastMetadata.getBroadcastCode(), + StandardCharsets.UTF_8); + String passwordText = + getString( + R.string.audio_streams_qr_code_page_password, + password); + ((TextView) view.requireViewById(R.id.password)) + .setText(passwordText); + } + TextView summaryView = view.requireViewById(android.R.id.summary); + String summary = + getString( + R.string.audio_streams_qr_code_page_description, + broadcastMetadata.getBroadcastName()); + summaryView.setText(summary); + }); + }); } private Optional getQrCodeBitmap(@Nullable BluetoothLeBroadcastMetadata metadata) { if (metadata == null) { - Log.d(TAG, "onCreateView: broadcastMetadata is empty!"); + Log.d(TAG, "getQrCodeBitmap: broadcastMetadata is empty!"); return Optional.empty(); } String metadataStr = BluetoothLeBroadcastMetadataExt.INSTANCE.toQrCodeString(metadata); if (metadataStr.isEmpty()) { - Log.d(TAG, "onCreateView: metadataStr is empty!"); + Log.d(TAG, "getQrCodeBitmap: metadataStr is empty!"); return Optional.empty(); } - Log.i(TAG, "onCreateView: metadataStr : " + metadataStr); + Log.d(TAG, "getQrCodeBitmap: metadata : " + metadata); try { int qrcodeSize = - getContext() - .getResources() - .getDimensionPixelSize(R.dimen.audio_streams_qrcode_size); + getResources().getDimensionPixelSize(R.dimen.audio_streams_qrcode_size); Bitmap bitmap = QrCodeGenerator.encodeQrCode(metadataStr, qrcodeSize); return Optional.of(bitmap); } catch (WriterException e) { Log.d( TAG, - "onCreateView: broadcastMetadata " + "getQrCodeBitmap: broadcastMetadata " + metadata + " qrCode generation exception " + e); @@ -122,13 +137,13 @@ public class AudioStreamsQrCodeFragment extends InstrumentedFragment { return null; } - BluetoothLeBroadcastMetadata metadata = - localBluetoothLeBroadcast.getLatestBluetoothLeBroadcastMetadata(); - if (metadata == null) { + List metadata = + localBluetoothLeBroadcast.getAllBroadcastMetadata(); + if (metadata.isEmpty()) { Log.d(TAG, "getBroadcastMetadataQrCode: metadata is null!"); return null; } - return metadata; + return metadata.get(0); } } diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsQrCodeScanFragment.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsQrCodeScanFragment.java index e9b9ff3bb59..8df4317e9f7 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsQrCodeScanFragment.java +++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsQrCodeScanFragment.java @@ -44,6 +44,7 @@ import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import com.android.settings.R; import com.android.settings.bluetooth.Utils; @@ -62,8 +63,8 @@ public class AudioStreamsQrCodeScanFragment extends InstrumentedFragment private static final int MESSAGE_HIDE_ERROR_MESSAGE = 1; private static final int MESSAGE_SHOW_ERROR_MESSAGE = 2; private static final int MESSAGE_SCAN_BROADCAST_SUCCESS = 3; - private static final long SHOW_ERROR_MESSAGE_INTERVAL = 10000; - private static final long SHOW_SUCCESS_SQUARE_INTERVAL = 1000; + @VisibleForTesting static final long SHOW_ERROR_MESSAGE_INTERVAL = 10000; + @VisibleForTesting static final long SHOW_SUCCESS_SQUARE_INTERVAL = 1000; private static final Duration VIBRATE_DURATION_QR_CODE_RECOGNITION = Duration.ofMillis(3); private final Handler mHandler = new Handler(Looper.getMainLooper()) { diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsScanQrCodeController.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsScanQrCodeController.java index 7f5c1e91088..5f50be72790 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsScanQrCodeController.java +++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsScanQrCodeController.java @@ -22,6 +22,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; @@ -32,7 +33,6 @@ import com.android.settings.bluetooth.Utils; import com.android.settings.core.BasePreferenceController; import com.android.settings.core.SubSettingLauncher; import com.android.settingslib.bluetooth.BluetoothCallback; -import com.android.settingslib.bluetooth.BluetoothUtils; import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.settingslib.bluetooth.LocalBluetoothManager; import com.android.settingslib.utils.ThreadUtils; @@ -41,9 +41,10 @@ public class AudioStreamsScanQrCodeController extends BasePreferenceController implements DefaultLifecycleObserver { static final int REQUEST_SCAN_BT_BROADCAST_QR_CODE = 0; private static final String TAG = "AudioStreamsProgressCategoryController"; - private static final boolean DEBUG = BluetoothUtils.D; - private static final String KEY = "audio_streams_scan_qr_code"; - private final BluetoothCallback mBluetoothCallback = + @VisibleForTesting static final String KEY = "audio_streams_scan_qr_code"; + + @VisibleForTesting + final BluetoothCallback mBluetoothCallback = new BluetoothCallback() { @Override public void onActiveDeviceChanged( diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsQrCodeFragmentTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsQrCodeFragmentTest.java new file mode 100644 index 00000000000..7d85b7ad2ab --- /dev/null +++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsQrCodeFragmentTest.java @@ -0,0 +1,140 @@ +/* + * 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.audiostreams; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.when; + +import static java.util.Collections.emptyList; +import static java.util.Collections.list; + +import android.app.settings.SettingsEnums; +import android.bluetooth.BluetoothLeBroadcastMetadata; +import android.content.Context; +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.fragment.app.FragmentActivity; +import androidx.test.core.app.ApplicationProvider; + +import com.android.settings.R; +import com.android.settings.bluetooth.Utils; +import com.android.settings.testutils.shadow.ShadowBluetoothUtils; +import com.android.settingslib.bluetooth.BluetoothEventManager; +import com.android.settingslib.bluetooth.BluetoothLeBroadcastMetadataExt; +import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast; +import com.android.settingslib.bluetooth.LocalBluetoothManager; +import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; + +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.shadows.androidx.fragment.FragmentController; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +@RunWith(RobolectricTestRunner.class) +@Config( + shadows = { + ShadowBluetoothUtils.class, + }) +public class AudioStreamsQrCodeFragmentTest { + @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule(); + private static final String VALID_METADATA = + "BLUETOOTH:UUID:184F;BN:VGVzdA==;AT:1;AD:00A1A1A1A1A1;BI:1E240;BC:VGVzdENvZGU=;" + + "MD:BgNwVGVzdA==;AS:1;PI:A0;NS:1;BS:3;NB:2;SM:BQNUZXN0BARlbmc=;;"; + @Mock private LocalBluetoothManager mLocalBtManager; + @Mock private BluetoothEventManager mBtEventManager; + @Mock private LocalBluetoothProfileManager mBtProfileManager; + @Mock private LocalBluetoothLeBroadcast mBroadcast; + private Context mContext; + private AudioStreamsQrCodeFragment mFragment; + + @Before + public void setUp() { + ShadowBluetoothUtils.sLocalBluetoothManager = mLocalBtManager; + LocalBluetoothManager btManager = Utils.getLocalBtManager(mContext); + when(btManager.getEventManager()).thenReturn(mBtEventManager); + when(btManager.getProfileManager()).thenReturn(mBtProfileManager); + when(mBtProfileManager.getLeAudioBroadcastProfile()).thenReturn(mBroadcast); + when(mBroadcast.getAllBroadcastMetadata()).thenReturn(emptyList()); + mContext = ApplicationProvider.getApplicationContext(); + mFragment = new AudioStreamsQrCodeFragment(); + } + + @After + public void tearDown() { + ShadowBluetoothUtils.reset(); + } + + @Test + public void getMetricsCategory_returnEnum() { + assertThat(mFragment.getMetricsCategory()).isEqualTo(SettingsEnums.AUDIO_STREAM_QR_CODE); + } + + @Test + public void onCreateView_noMetadata_noQrCode() { + List list = new ArrayList<>(); + when(mBroadcast.getAllBroadcastMetadata()).thenReturn(list); + FragmentController.setupFragment( + mFragment, FragmentActivity.class, /* containerViewId= */ 0, /* bundle= */ null); + View view = mFragment.getView(); + + assertThat(view).isNotNull(); + ImageView qrCodeView = view.findViewById(R.id.qrcode_view); + TextView passwordView = view.requireViewById(R.id.password); + assertThat(qrCodeView).isNotNull(); + assertThat(qrCodeView.getDrawable()).isNull(); + assertThat(passwordView).isNotNull(); + assertThat(passwordView.getText().toString()).isEqualTo(""); + } + + @Test + public void onCreateView_hasMetadata_hasQrCode() { + var metadata = + BluetoothLeBroadcastMetadataExt.INSTANCE.convertToBroadcastMetadata(VALID_METADATA); + List list = new ArrayList<>(); + list.add(metadata); + when(mBroadcast.getAllBroadcastMetadata()).thenReturn(list); + FragmentController.setupFragment( + mFragment, FragmentActivity.class, /* containerViewId= */ 0, /* bundle= */ null); + View view = mFragment.getView(); + + assertThat(view).isNotNull(); + ImageView qrCodeView = view.findViewById(R.id.qrcode_view); + TextView passwordView = view.requireViewById(R.id.password); + assertThat(qrCodeView).isNotNull(); + assertThat(qrCodeView.getDrawable()).isNotNull(); + assertThat(passwordView).isNotNull(); + assertThat(passwordView.getText().toString()) + .isEqualTo( + mContext.getString( + R.string.audio_streams_qr_code_page_password, + new String(metadata.getBroadcastCode(), StandardCharsets.UTF_8))); + } +} diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsQrCodeScanFragmentTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsQrCodeScanFragmentTest.java new file mode 100644 index 00000000000..0dd495f93fe --- /dev/null +++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsQrCodeScanFragmentTest.java @@ -0,0 +1,210 @@ +/* + * 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.audiostreams; + +import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsQrCodeScanFragment.SHOW_ERROR_MESSAGE_INTERVAL; +import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsQrCodeScanFragment.SHOW_SUCCESS_SQUARE_INTERVAL; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.graphics.SurfaceTexture; +import android.view.TextureView; +import android.view.View; +import android.widget.TextView; + +import androidx.fragment.app.FragmentActivity; +import androidx.test.core.app.ApplicationProvider; + +import com.android.settings.R; +import com.android.settings.connecteddevice.audiosharing.audiostreams.testshadows.ShadowAudioStreamsHelper; +import com.android.settings.connecteddevice.audiosharing.audiostreams.testshadows.ShadowQrCamera; +import com.android.settingslib.bluetooth.CachedBluetoothDevice; +import com.android.settingslib.qrcode.QrCamera; + +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.shadows.ShadowLooper; +import org.robolectric.shadows.androidx.fragment.FragmentController; + +import java.util.concurrent.TimeUnit; + +@RunWith(RobolectricTestRunner.class) +@Config( + shadows = { + ShadowAudioStreamsHelper.class, + ShadowQrCamera.class, + }) +public class AudioStreamsQrCodeScanFragmentTest { + @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule(); + private static final String VALID_METADATA = + "BLUETOOTH:UUID:184F;BN:VGVzdA==;AT:1;AD:00A1A1A1A1A1;BI:1E240;BC:VGVzdENvZGU=;" + + "MD:BgNwVGVzdA==;AS:1;PI:A0;NS:1;BS:3;NB:2;SM:BQNUZXN0BARlbmc=;;"; + private static final String DEVICE_NAME = "device_name"; + @Mock private CachedBluetoothDevice mDevice; + @Mock private QrCamera mQrCamera; + @Mock private SurfaceTexture mSurfaceTexture; + private Context mContext; + private AudioStreamsQrCodeScanFragment mFragment; + + @Before + public void setUp() { + ShadowAudioStreamsHelper.setCachedBluetoothDeviceInSharingOrLeConnected(mDevice); + ShadowQrCamera.setUseMock(mQrCamera); + when(mDevice.getName()).thenReturn(DEVICE_NAME); + mContext = ApplicationProvider.getApplicationContext(); + mFragment = new AudioStreamsQrCodeScanFragment(); + } + + @After + public void tearDown() { + ShadowAudioStreamsHelper.reset(); + ShadowQrCamera.reset(); + } + + @Test + public void getMetricsCategory_returnEnum() { + assertThat(mFragment.getMetricsCategory()) + .isEqualTo(SettingsEnums.AUDIO_STREAM_QR_CODE_SCAN); + } + + @Test + public void onCreateView_createLayout() { + FragmentController.setupFragment( + mFragment, FragmentActivity.class, /* containerViewId= */ 0, /* bundle= */ null); + ShadowLooper.idleMainLooper(); + View view = mFragment.getView(); + + assertThat(view).isNotNull(); + TextureView textureView = view.findViewById(R.id.preview_view); + assertThat(textureView).isNotNull(); + assertThat(textureView.getSurfaceTextureListener()).isNotNull(); + assertThat(textureView.getOutlineProvider()).isNotNull(); + assertThat(textureView.getClipToOutline()).isTrue(); + + TextView errorMessage = view.findViewById(R.id.error_message); + assertThat(errorMessage).isNotNull(); + assertThat(errorMessage.getText().toString()).isEqualTo(""); + + TextView summary = view.findViewById(android.R.id.summary); + assertThat(summary).isNotNull(); + assertThat(summary.getText().toString()) + .isEqualTo( + mContext.getString( + R.string.audio_streams_main_page_qr_code_scanner_summary, + DEVICE_NAME)); + } + + @Test + public void surfaceTextureListener_startAndStopQrCamera() { + FragmentController.setupFragment( + mFragment, FragmentActivity.class, /* containerViewId= */ 0, /* bundle= */ null); + ShadowLooper.idleMainLooper(); + View view = mFragment.getView(); + + assertThat(view).isNotNull(); + TextureView textureView = view.findViewById(R.id.preview_view); + assertThat(textureView).isNotNull(); + TextureView.SurfaceTextureListener listener = textureView.getSurfaceTextureListener(); + + assertThat(listener).isNotNull(); + listener.onSurfaceTextureAvailable(mSurfaceTexture, 50, 50); + verify(mQrCamera).start(any()); + + listener.onSurfaceTextureSizeChanged(mSurfaceTexture, 150, 150); + listener.onSurfaceTextureUpdated(mSurfaceTexture); + listener.onSurfaceTextureDestroyed(mSurfaceTexture); + verify(mQrCamera).stop(); + + mFragment.handleCameraFailure(); + verify(mQrCamera).stop(); + } + + @Test + public void scannerCallback_sendSuccessMessage() { + FragmentController.setupFragment( + mFragment, FragmentActivity.class, /* containerViewId= */ 0, /* bundle= */ null); + View view = mFragment.getView(); + ShadowLooper.idleMainLooper(); + + assertThat(view).isNotNull(); + TextureView textureView = view.findViewById(R.id.preview_view); + TextView errorMessage = view.findViewById(R.id.error_message); + + mFragment.handleSuccessfulResult("qrcode"); + ShadowLooper.idleMainLooper(SHOW_SUCCESS_SQUARE_INTERVAL, TimeUnit.MILLISECONDS); + + assertThat(textureView).isNotNull(); + assertThat(textureView.getVisibility()).isEqualTo(View.INVISIBLE); + assertThat(errorMessage).isNotNull(); + assertThat(errorMessage.getVisibility()).isEqualTo(View.INVISIBLE); + } + + @Test + public void scannerCallback_isValid() { + Boolean result = mFragment.isValid(VALID_METADATA); + assertThat(result).isTrue(); + } + + @Test + public void scannerCallback_isInvalid_showErrorThenHide() { + FragmentController.setupFragment( + mFragment, FragmentActivity.class, /* containerViewId= */ 0, /* bundle= */ null); + Boolean result = mFragment.isValid("invalid"); + assertThat(result).isFalse(); + + ShadowLooper.idleMainLooper(); + View view = mFragment.getView(); + assertThat(view).isNotNull(); + TextView errorMessage = view.findViewById(R.id.error_message); + assertThat(errorMessage).isNotNull(); + assertThat(errorMessage.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(errorMessage.getText().toString()) + .isEqualTo(mContext.getString(R.string.audio_streams_qr_code_is_not_valid_format)); + + ShadowLooper.idleMainLooper(SHOW_ERROR_MESSAGE_INTERVAL, TimeUnit.MILLISECONDS); + assertThat(errorMessage.getVisibility()).isEqualTo(View.INVISIBLE); + } + + @Test + public void getViewSize_getSize() { + FragmentController.setupFragment( + mFragment, FragmentActivity.class, /* containerViewId= */ 0, /* bundle= */ null); + ShadowLooper.idleMainLooper(); + View view = mFragment.getView(); + assertThat(view).isNotNull(); + TextureView textureView = view.findViewById(R.id.preview_view); + assertThat(textureView).isNotNull(); + + var result = mFragment.getViewSize(); + assertThat(result.getWidth()).isEqualTo(textureView.getWidth()); + assertThat(result.getHeight()).isEqualTo(textureView.getHeight()); + } +} diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsScanQrCodeControllerTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsScanQrCodeControllerTest.java new file mode 100644 index 00000000000..4990f26ac22 --- /dev/null +++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsScanQrCodeControllerTest.java @@ -0,0 +1,165 @@ +/* + * 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.audiostreams; + +import static com.android.settings.core.BasePreferenceController.AVAILABLE; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.bluetooth.BluetoothProfile; +import android.content.Context; + +import androidx.lifecycle.LifecycleOwner; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; +import androidx.test.core.app.ApplicationProvider; + +import com.android.settings.connecteddevice.audiosharing.audiostreams.testshadows.ShadowAudioStreamsHelper; +import com.android.settings.testutils.shadow.ShadowBluetoothUtils; +import com.android.settingslib.bluetooth.BluetoothEventManager; +import com.android.settingslib.bluetooth.CachedBluetoothDevice; +import com.android.settingslib.bluetooth.LocalBluetoothManager; +import com.android.settingslib.core.lifecycle.Lifecycle; + +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; + +@RunWith(RobolectricTestRunner.class) +@Config( + shadows = { + ShadowBluetoothUtils.class, + ShadowAudioStreamsHelper.class, + }) +public class AudioStreamsScanQrCodeControllerTest { + @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule(); + @Mock private LocalBluetoothManager mLocalBtManager; + @Mock private BluetoothEventManager mBluetoothEventManager; + @Mock private PreferenceScreen mScreen; + @Mock private AudioStreamsDashboardFragment mFragment; + @Mock private CachedBluetoothDevice mDevice; + private Preference mPreference; + private Lifecycle mLifecycle; + private LifecycleOwner mLifecycleOwner; + private AudioStreamsScanQrCodeController mController; + private Context mContext; + + @Before + public void setUp() { + ShadowBluetoothUtils.sLocalBluetoothManager = mLocalBtManager; + when(mLocalBtManager.getEventManager()).thenReturn(mBluetoothEventManager); + mLifecycleOwner = () -> mLifecycle; + mLifecycle = new Lifecycle(mLifecycleOwner); + mContext = ApplicationProvider.getApplicationContext(); + mController = + new AudioStreamsScanQrCodeController( + mContext, AudioStreamsScanQrCodeController.KEY); + mPreference = spy(new Preference(mContext)); + when(mScreen.findPreference(anyString())).thenReturn(mPreference); + when(mPreference.getKey()).thenReturn(AudioStreamsScanQrCodeController.KEY); + } + + @After + public void tearDown() { + ShadowAudioStreamsHelper.reset(); + ShadowBluetoothUtils.reset(); + } + + @Test + public void getAvailabilityStatus() { + assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE); + } + + @Test + public void getPreferenceKey() { + var key = mController.getPreferenceKey(); + + assertThat(key).isEqualTo(AudioStreamsScanQrCodeController.KEY); + } + + @Test + public void onStart_registerCallback() { + mController.onStart(mLifecycleOwner); + + verify(mBluetoothEventManager).registerCallback(any()); + } + + @Test + public void onStop_unregisterCallback() { + mController.onStop(mLifecycleOwner); + + verify(mBluetoothEventManager).unregisterCallback(any()); + } + + @Test + public void onDisplayPreference_setOnclick() { + mController.displayPreference(mScreen); + + verify(mPreference).setOnPreferenceClickListener(any()); + } + + @Test + public void onPreferenceClick_noFragment_doNothing() { + mController.displayPreference(mScreen); + + var listener = mPreference.getOnPreferenceClickListener(); + assertThat(listener).isNotNull(); + var clicked = listener.onPreferenceClick(mPreference); + assertThat(clicked).isFalse(); + } + + @Test + public void onPreferenceClick_hasFragment_launchSubSetting() { + mController.displayPreference(mScreen); + mController.setFragment(mFragment); + + var listener = mPreference.getOnPreferenceClickListener(); + assertThat(listener).isNotNull(); + var clicked = listener.onPreferenceClick(mPreference); + assertThat(clicked).isTrue(); + } + + @Test + public void updateVisibility_noConnected_invisible() { + mController.displayPreference(mScreen); + mController.mBluetoothCallback.onActiveDeviceChanged(mDevice, BluetoothProfile.LE_AUDIO); + + assertThat(mPreference.isVisible()).isFalse(); + } + + @Test + public void updateVisibility_hasConnected_visible() { + mController.displayPreference(mScreen); + ShadowAudioStreamsHelper.setCachedBluetoothDeviceInSharingOrLeConnected(mDevice); + mController.mBluetoothCallback.onActiveDeviceChanged(mDevice, BluetoothProfile.LE_AUDIO); + + assertThat(mPreference.isVisible()).isTrue(); + } +} diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/testshadows/ShadowQrCamera.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/testshadows/ShadowQrCamera.java new file mode 100644 index 00000000000..032c91f8ea3 --- /dev/null +++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/testshadows/ShadowQrCamera.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.audiostreams.testshadows; + +import android.graphics.SurfaceTexture; + +import com.android.settingslib.qrcode.QrCamera; + +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; +import org.robolectric.annotation.Resetter; + +@Implements(value = QrCamera.class, callThroughByDefault = false) +public class ShadowQrCamera { + + private static QrCamera sMockQrCamera; + + public static void setUseMock(QrCamera mockQrCamera) { + sMockQrCamera = mockQrCamera; + } + + /** Start camera */ + @Implementation + public void start(SurfaceTexture surface) { + sMockQrCamera.start(surface); + } + + /** Stop camera */ + @Implementation + public void stop() { + sMockQrCamera.stop(); + } + + /** Reset static fields */ + @Resetter + public static void reset() { + sMockQrCamera = null; + } +}