[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;
+    }
+}