[Audiosharing] Add tests for qrcode related classes.
Test: atest -c com.android.settings.connecteddevice.audiosharing.audiostreams
Bug: 308368124
Change-Id: I74caacf43a23bbd0a6da44af24a1be4dd9544a5d
diff --git a/res/xml/bluetooth_audio_streams_qr_code.xml b/res/xml/bluetooth_audio_streams_qr_code.xml
index a098845..5ec5505 100644
--- a/res/xml/bluetooth_audio_streams_qr_code.xml
+++ b/res/xml/bluetooth_audio_streams_qr_code.xml
@@ -47,8 +47,7 @@
<ImageView
android:id="@+id/qrcode_view"
android:layout_width="@dimen/qrcode_size"
- android:layout_height="@dimen/qrcode_size"
- android:src="@android:color/transparent"/>
+ android:layout_height="@dimen/qrcode_size"/>
<TextView
android:id="@+id/password"
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsDashboardFragment.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsDashboardFragment.java
index d643b89..ce32cdb 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsDashboardFragment.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsDashboardFragment.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2023 The Android Open Source Project
+ * 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.
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsQrCodeFragment.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsQrCodeFragment.java
index 2bdc6ca..e4c0794 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsQrCodeFragment.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsQrCodeFragment.java
@@ -27,6 +27,7 @@
import android.widget.ImageView;
import android.widget.TextView;
+import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.settings.R;
@@ -35,10 +36,12 @@
import com.android.settingslib.bluetooth.BluetoothLeBroadcastMetadataExt;
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast;
import com.android.settingslib.qrcode.QrCodeGenerator;
+import com.android.settingslib.utils.ThreadUtils;
import com.google.zxing.WriterException;
import java.nio.charset.StandardCharsets;
+import java.util.List;
import java.util.Optional;
public class AudioStreamsQrCodeFragment extends InstrumentedFragment {
@@ -52,57 +55,69 @@
@Override
public final View onCreateView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
- View view = inflater.inflate(R.xml.bluetooth_audio_streams_qr_code, container, false);
+ return inflater.inflate(R.xml.bluetooth_audio_streams_qr_code, container, false);
+ }
- BluetoothLeBroadcastMetadata broadcastMetadata = getBroadcastMetadata();
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ var unused = ThreadUtils.postOnBackgroundThread(
+ () -> {
+ BluetoothLeBroadcastMetadata broadcastMetadata = getBroadcastMetadata();
+ if (broadcastMetadata == null) {
+ return;
+ }
+ Bitmap bm = getQrCodeBitmap(broadcastMetadata).orElse(null);
+ if (bm == null) {
+ return;
+ }
- if (broadcastMetadata != null) {
- Optional<Bitmap> 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<Bitmap> 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 @@
return null;
}
- BluetoothLeBroadcastMetadata metadata =
- localBluetoothLeBroadcast.getLatestBluetoothLeBroadcastMetadata();
- if (metadata == null) {
+ List<BluetoothLeBroadcastMetadata> 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 e9b9ff3..8df4317 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 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 @@
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 7f5c1e9..5f50be7 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 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.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 @@
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 0000000..7d85b7a
--- /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<BluetoothLeBroadcastMetadata> 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<BluetoothLeBroadcastMetadata> 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 0000000..0dd495f
--- /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 0000000..4990f26
--- /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 0000000..032c91f
--- /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;
+ }
+}