chromium/chrome/android/javatests/src/org/chromium/chrome/browser/device_dialog/BluetoothChooserDialogTest.java

// Copyright 2016 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.chrome.browser.device_dialog;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import android.Manifest;
import android.app.Activity;
import android.app.Dialog;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.location.LocationManager;
import android.os.Build;
import android.view.View;
import android.widget.Button;
import android.widget.ListView;

import androidx.test.filters.SmallTest;

import org.hamcrest.Matchers;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;

import org.chromium.base.ThreadUtils;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.Criteria;
import org.chromium.base.test.util.CriteriaHelper;
import org.chromium.base.test.util.DisabledTest;
import org.chromium.base.test.util.JniMocker;
import org.chromium.base.test.util.MaxAndroidSdkLevel;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.browser.profiles.ProfileManager;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
import org.chromium.chrome.test.R;
import org.chromium.chrome.test.batch.BlankCTATabInitialStateRule;
import org.chromium.components.location.LocationUtils;
import org.chromium.components.permissions.BluetoothChooserDialog;
import org.chromium.components.permissions.BluetoothChooserDialogJni;
import org.chromium.components.permissions.DeviceItemAdapter;
import org.chromium.components.permissions.ItemChooserDialog;
import org.chromium.components.security_state.ConnectionSecurityLevel;
import org.chromium.content_public.browser.bluetooth.BluetoothChooserEvent;
import org.chromium.content_public.browser.test.util.TouchCommon;
import org.chromium.ui.base.ActivityWindowAndroid;
import org.chromium.ui.base.WindowAndroid;
import org.chromium.ui.modaldialog.ModalDialogManager;
import org.chromium.ui.permissions.AndroidPermissionDelegate;
import org.chromium.ui.permissions.PermissionCallback;
import org.chromium.ui.widget.TextViewWithClickableSpans;

import java.lang.ref.WeakReference;
import java.util.Arrays;
import java.util.concurrent.Callable;

/**
 * Tests for the BluetoothChooserDialog class.
 *
 * <p>TODO(crbug.com/40187298): Componentize this test.
 */
@RunWith(ChromeJUnit4ClassRunner.class)
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
// TODO(crbug.com/344665244): Failing when batched, batch this again.
public class BluetoothChooserDialogTest {
    public static final String DEVICE_DIALOG_BATCH_NAME = "device_dialog";

    @ClassRule
    public static final ChromeTabbedActivityTestRule sActivityTestRule =
            new ChromeTabbedActivityTestRule();

    @Rule
    public final BlankCTATabInitialStateRule mInitialStateRule =
            new BlankCTATabInitialStateRule(sActivityTestRule, false);

    @Rule public JniMocker mocker = new JniMocker();

    private ActivityWindowAndroid mWindowAndroid;
    private FakeLocationUtils mLocationUtils;
    private BluetoothChooserDialog mChooserDialog;

    private int mFinishedEventType = -1;
    private String mFinishedDeviceId;
    private int mRestartSearchCount;

    // Unused member variables to avoid Java optimizer issues with Mockito.
    @Mock ModalDialogManager mMockModalDialogManager;
    @Mock Activity mMockActivity;
    @Mock WindowAndroid mMockWindowAndroid;

    private class TestBluetoothChooserDialogJni implements BluetoothChooserDialog.Natives {
        private BluetoothChooserDialog mBluetoothChooserDialog;

        TestBluetoothChooserDialogJni(BluetoothChooserDialog dialog) {
            mBluetoothChooserDialog = dialog;
        }

        @Override
        public void onDialogFinished(
                long nativeBluetoothChooserAndroid, int eventType, String deviceId) {
            Assert.assertEquals(
                    nativeBluetoothChooserAndroid,
                    mBluetoothChooserDialog.mNativeBluetoothChooserDialogPtr);
            Assert.assertEquals(mFinishedEventType, -1);
            mFinishedEventType = eventType;
            mFinishedDeviceId = deviceId;
            // The native code calls closeDialog() when OnDialogFinished is called.
            mBluetoothChooserDialog.closeDialog();
        }

        @Override
        public void restartSearch(long nativeBluetoothChooserAndroid) {
            Assert.assertTrue(mBluetoothChooserDialog.mNativeBluetoothChooserDialogPtr != 0);
            mRestartSearchCount++;
        }

        @Override
        public void showBluetoothOverviewLink(long nativeBluetoothChooserAndroid) {
            // We shouldn't be running native functions if the native class has been destroyed.
            Assert.assertTrue(mBluetoothChooserDialog.mNativeBluetoothChooserDialogPtr != 0);
        }

        @Override
        public void showBluetoothAdapterOffLink(long nativeBluetoothChooserAndroid) {
            // We shouldn't be running native functions if the native class has been destroyed.
            Assert.assertTrue(mBluetoothChooserDialog.mNativeBluetoothChooserDialogPtr != 0);
        }

        @Override
        public void showNeedLocationPermissionLink(long nativeBluetoothChooserAndroid) {
            // We shouldn't be running native functions if the native class has been destroyed.
            Assert.assertTrue(mBluetoothChooserDialog.mNativeBluetoothChooserDialogPtr != 0);
        }
    }

    @Before
    public void setUp() throws Exception {
        mLocationUtils = new FakeLocationUtils();
        LocationUtils.setFactory(() -> mLocationUtils);
        mChooserDialog = createDialog();
        mocker.mock(
                BluetoothChooserDialogJni.TEST_HOOKS,
                new TestBluetoothChooserDialogJni(mChooserDialog));
    }

    @After
    public void tearDown() {
        LocationUtils.setFactory(null);
    }

    private BluetoothChooserDialog createDialog() {
        return ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mWindowAndroid = sActivityTestRule.getActivity().getWindowAndroid();
                    BluetoothChooserDialog dialog =
                            new BluetoothChooserDialog(
                                    mWindowAndroid,
                                    "https://origin.example.com/",
                                    ConnectionSecurityLevel.SECURE,
                                    new ChromeBluetoothChooserAndroidDelegate(
                                            ProfileManager.getLastUsedRegularProfile()),
                                    /* nativeBluetoothChooserDialogPtr= */ 42);
                    dialog.show();
                    return dialog;
                });
    }

    private void selectItem(int position) {
        final Dialog dialog = mChooserDialog.mItemChooserDialog.getDialogForTesting();
        final ListView items = dialog.findViewById(R.id.items);
        final Button button = dialog.findViewById(R.id.positive);

        clickItemAtPosition(items, position - 1);

        CriteriaHelper.pollUiThread(() -> button.isEnabled());
        // Make sure the button is properly rendered before clicking.
        CriteriaHelper.pollUiThread(
                () -> {
                    Criteria.checkThat(button.getHeight(), Matchers.greaterThan(0));
                });

        TouchCommon.singleClickView(button);

        CriteriaHelper.pollUiThread(() -> Criteria.checkThat(mFinishedEventType, Matchers.not(-1)));
    }

    private static void clickItemAtPosition(ListView listView, int position) {
        CriteriaHelper.pollUiThread(() -> listView.getChildAt(0) != null);

        Callable<Boolean> isVisible =
                () -> {
                    int visibleStart = listView.getFirstVisiblePosition();
                    int visibleEnd = visibleStart + listView.getChildCount() - 1;
                    return position >= visibleStart && position <= visibleEnd;
                };

        if (!ThreadUtils.runOnUiThreadBlocking(isVisible)) {
            ThreadUtils.runOnUiThreadBlocking(() -> listView.setSelection(position));
            CriteriaHelper.pollUiThread(isVisible);
        }

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    TouchCommon.singleClickView(
                            listView.getChildAt(position - listView.getFirstVisiblePosition()));
                });
    }

    /**
     * The messages include <*link*> ... </*link*> sections that are used to create clickable spans.
     * For testing the messages, this function returns the raw string without the tags.
     */
    private static String removeLinkTags(String message) {
        return message.replaceAll("</?[^>]*link[^>]*>", "");
    }

    /**
     * The helper function help to determine whether |requestedPermissions| pass correct permissions
     * before and from Android S.
     */
    private static boolean checkRequestedPermissions(String[] requestedPermissions) {
        if (requestedPermissions == null) return false;
        String[] expectedPermissionBeforeS = {Manifest.permission.ACCESS_FINE_LOCATION};
        String[] expectedPermissionFromS = {
            Manifest.permission.BLUETOOTH_CONNECT, Manifest.permission.BLUETOOTH_SCAN
        };
        String[] expected =
                Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
                        ? expectedPermissionFromS
                        : expectedPermissionBeforeS;
        String[] copied = Arrays.copyOf(requestedPermissions, requestedPermissions.length);
        Arrays.sort(expected);
        Arrays.sort(copied);
        return Arrays.equals(expected, copied);
    }

    @Test
    @SmallTest
    public void testCancel() {
        ItemChooserDialog itemChooser = mChooserDialog.mItemChooserDialog;
        Dialog dialog = itemChooser.getDialogForTesting();
        Assert.assertTrue(dialog.isShowing());

        TextViewWithClickableSpans statusView = dialog.findViewById(R.id.status);
        final ListView items = dialog.findViewById(R.id.items);
        final Button button = dialog.findViewById(R.id.positive);

        // Before we add items to the dialog, the 'searching' message should be
        // showing, the Commit button should be disabled and the list view hidden.
        Assert.assertEquals(
                removeLinkTags(
                        sActivityTestRule.getActivity().getString(R.string.bluetooth_searching)),
                statusView.getText().toString());
        Assert.assertFalse(button.isEnabled());
        Assert.assertEquals(View.GONE, items.getVisibility());

        dialog.cancel();

        CriteriaHelper.pollUiThread(() -> Criteria.checkThat(mFinishedEventType, Matchers.not(-1)));

        Assert.assertEquals(BluetoothChooserEvent.CANCELLED, mFinishedEventType);
        Assert.assertEquals("", mFinishedDeviceId);
    }

    @Test
    @SmallTest
    public void testDismiss() {
        ItemChooserDialog itemChooser = mChooserDialog.mItemChooserDialog;
        Dialog dialog = itemChooser.getDialogForTesting();
        Assert.assertTrue(dialog.isShowing());

        TextViewWithClickableSpans statusView = dialog.findViewById(R.id.status);
        final ListView items = dialog.findViewById(R.id.items);
        final Button button = dialog.findViewById(R.id.positive);

        // Before we add items to the dialog, the 'searching' message should be
        // showing, the Commit button should be disabled and the list view hidden.
        Assert.assertEquals(
                removeLinkTags(
                        sActivityTestRule.getActivity().getString(R.string.bluetooth_searching)),
                statusView.getText().toString());
        Assert.assertFalse(button.isEnabled());
        Assert.assertEquals(View.GONE, items.getVisibility());

        dialog.dismiss();

        CriteriaHelper.pollUiThread(() -> Criteria.checkThat(mFinishedEventType, Matchers.not(-1)));

        Assert.assertEquals(BluetoothChooserEvent.CANCELLED, mFinishedEventType);
        Assert.assertEquals("", mFinishedDeviceId);
    }

    @Test
    @SmallTest
    public void testSelectItem() {
        Dialog dialog = mChooserDialog.mItemChooserDialog.getDialogForTesting();
        Assert.assertTrue(dialog.isShowing());

        TextViewWithClickableSpans statusView = dialog.findViewById(R.id.status);
        final View items = dialog.findViewById(R.id.items);
        final Button button = dialog.findViewById(R.id.positive);
        final View progress = dialog.findViewById(R.id.progress);

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    // Add non-connected device with no signal strength.
                    mChooserDialog.addOrUpdateDevice(
                            "id-1",
                            "Name 1",
                            /* isGATTConnected= */ false,
                            /* signalStrengthLevel= */ -1);
                    // Add connected device with no signal strength.
                    mChooserDialog.addOrUpdateDevice(
                            "id-2",
                            "Name 2",
                            /* isGATTConnected= */ true,
                            /* signalStrengthLevel= */ -1);
                    // Add non-connected device with signal strength level 1.
                    mChooserDialog.addOrUpdateDevice(
                            "id-3",
                            "Name 3",
                            /* isGATTConnected= */ false,
                            /* signalStrengthLevel= */ 1);
                    // Add connected device with signal strength level 1.
                    mChooserDialog.addOrUpdateDevice(
                            "id-4",
                            "Name 4",
                            /* isGATTConnected= */ true,
                            /* signalStrengthLevel= */ 1);
                });

        // After adding items to the dialog, the help message should be showing,
        // the progress spinner should disappear, the Commit button should still
        // be disabled (since nothing's selected), and the list view should
        // show.
        Assert.assertEquals(
                removeLinkTags(
                        sActivityTestRule.getActivity().getString(R.string.bluetooth_searching)),
                statusView.getText().toString());
        Assert.assertFalse(button.isEnabled());
        Assert.assertEquals(View.VISIBLE, items.getVisibility());
        Assert.assertEquals(View.GONE, progress.getVisibility());

        DeviceItemAdapter itemAdapter =
                mChooserDialog.mItemChooserDialog.getItemAdapterForTesting();
        Assert.assertTrue(
                itemAdapter
                        .getItem(0)
                        .hasSameContents(
                                "id-1", "Name 1", /* icon= */ null, /* iconDescription= */ null));
        Assert.assertTrue(
                itemAdapter
                        .getItem(1)
                        .hasSameContents(
                                "id-2",
                                "Name 2",
                                mChooserDialog.mConnectedIcon,
                                mChooserDialog.mConnectedIconDescription));
        Assert.assertTrue(
                itemAdapter
                        .getItem(2)
                        .hasSameContents(
                                "id-3",
                                "Name 3",
                                mChooserDialog.mSignalStrengthLevelIcon[1],
                                sActivityTestRule
                                        .getActivity()
                                        .getResources()
                                        .getQuantityString(
                                                R.plurals.signal_strength_level_n_bars, 1, 1)));
        // We show the connected icon even if the device has a signal strength.
        Assert.assertTrue(
                itemAdapter
                        .getItem(3)
                        .hasSameContents(
                                "id-4",
                                "Name 4",
                                mChooserDialog.mConnectedIcon,
                                mChooserDialog.mConnectedIconDescription));

        selectItem(2);

        Assert.assertEquals(BluetoothChooserEvent.SELECTED, mFinishedEventType);
        Assert.assertEquals("id-2", mFinishedDeviceId);
    }

    @Test
    @SmallTest
    public void testNoPermission() {
        ItemChooserDialog itemChooser = mChooserDialog.mItemChooserDialog;
        Dialog dialog = itemChooser.getDialogForTesting();
        Assert.assertTrue(dialog.isShowing());

        final TextViewWithClickableSpans statusView = dialog.findViewById(R.id.status);
        final TextViewWithClickableSpans errorView = dialog.findViewById(R.id.not_found_message);
        final View items = dialog.findViewById(R.id.items);
        final Button button = dialog.findViewById(R.id.positive);
        final View progress = dialog.findViewById(R.id.progress);

        final TestAndroidPermissionDelegate permissionDelegate =
                new TestAndroidPermissionDelegate(dialog);
        mWindowAndroid.setAndroidPermissionDelegate(permissionDelegate);

        ThreadUtils.runOnUiThreadBlocking(
                () ->
                        mChooserDialog.notifyDiscoveryState(
                                BluetoothChooserDialog.DiscoveryMode.DISCOVERY_FAILED_TO_START));

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
            Assert.assertEquals(
                    removeLinkTags(
                            sActivityTestRule
                                    .getActivity()
                                    .getString(R.string.bluetooth_need_nearby_devices_permission)),
                    errorView.getText().toString());
        } else {
            Assert.assertEquals(
                    removeLinkTags(
                            sActivityTestRule
                                    .getActivity()
                                    .getString(R.string.bluetooth_need_location_permission)),
                    errorView.getText().toString());
        }

        Assert.assertEquals(
                removeLinkTags(
                        sActivityTestRule
                                .getActivity()
                                .getString(R.string.bluetooth_adapter_off_help)),
                statusView.getText().toString());
        Assert.assertFalse(button.isEnabled());
        Assert.assertEquals(View.VISIBLE, errorView.getVisibility());
        Assert.assertEquals(View.GONE, items.getVisibility());
        Assert.assertEquals(View.GONE, progress.getVisibility());

        ThreadUtils.runOnUiThreadBlocking(
                () -> errorView.getClickableSpans()[0].onClick(errorView));

        // Permission was requested.
        Assert.assertTrue(checkRequestedPermissions(permissionDelegate.mPermissionsRequested));
        Assert.assertNotNull(permissionDelegate.mCallback);

        // Grant permission.
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
            permissionDelegate.mBluetoothConnectGranted = true;
            permissionDelegate.mBluetoothScanGranted = true;
            ThreadUtils.runOnUiThreadBlocking(
                    () ->
                            permissionDelegate.mCallback.onRequestPermissionsResult(
                                    new String[] {
                                        Manifest.permission.BLUETOOTH_CONNECT,
                                        Manifest.permission.BLUETOOTH_SCAN
                                    },
                                    new int[] {PackageManager.PERMISSION_GRANTED}));

        } else {
            permissionDelegate.mLocationGranted = true;
            ThreadUtils.runOnUiThreadBlocking(
                    () ->
                            permissionDelegate.mCallback.onRequestPermissionsResult(
                                    new String[] {Manifest.permission.ACCESS_FINE_LOCATION},
                                    new int[] {PackageManager.PERMISSION_GRANTED}));
        }

        Assert.assertEquals(1, mRestartSearchCount);
        Assert.assertEquals(
                removeLinkTags(
                        sActivityTestRule.getActivity().getString(R.string.bluetooth_searching)),
                statusView.getText().toString());
        mChooserDialog.closeDialog();
    }

    @Test
    @SmallTest
    @MaxAndroidSdkLevel(Build.VERSION_CODES.R)
    public void testNoLocationServices() {
        ItemChooserDialog itemChooser = mChooserDialog.mItemChooserDialog;
        Dialog dialog = itemChooser.getDialogForTesting();
        Assert.assertTrue(dialog.isShowing());

        final TextViewWithClickableSpans statusView = dialog.findViewById(R.id.status);
        final TextViewWithClickableSpans errorView = dialog.findViewById(R.id.not_found_message);
        final View items = dialog.findViewById(R.id.items);
        final Button button = dialog.findViewById(R.id.positive);
        final View progress = dialog.findViewById(R.id.progress);

        final TestAndroidPermissionDelegate permissionDelegate =
                new TestAndroidPermissionDelegate(dialog);
        mWindowAndroid.setAndroidPermissionDelegate(permissionDelegate);

        // Grant permissions, and turn off location services.
        permissionDelegate.mLocationGranted = true;
        mLocationUtils.mSystemLocationSettingsEnabled = false;

        ThreadUtils.runOnUiThreadBlocking(
                () ->
                        mChooserDialog.notifyDiscoveryState(
                                BluetoothChooserDialog.DiscoveryMode.DISCOVERY_FAILED_TO_START));

        Assert.assertEquals(
                removeLinkTags(
                        sActivityTestRule
                                .getActivity()
                                .getString(R.string.bluetooth_need_location_services_on)),
                errorView.getText().toString());
        Assert.assertEquals(
                removeLinkTags(
                        sActivityTestRule
                                .getActivity()
                                .getString(R.string.bluetooth_need_location_permission_help)),
                statusView.getText().toString());
        Assert.assertFalse(button.isEnabled());
        Assert.assertEquals(View.VISIBLE, errorView.getVisibility());
        Assert.assertEquals(View.GONE, items.getVisibility());
        Assert.assertEquals(View.GONE, progress.getVisibility());

        // Turn on Location Services.
        mLocationUtils.mSystemLocationSettingsEnabled = true;
        ThreadUtils.runOnUiThreadBlocking(
                () ->
                        mChooserDialog.mLocationModeBroadcastReceiver.onReceive(
                                sActivityTestRule.getActivity(),
                                new Intent(LocationManager.MODE_CHANGED_ACTION)));

        Assert.assertEquals(1, mRestartSearchCount);
        Assert.assertEquals(
                removeLinkTags(
                        sActivityTestRule.getActivity().getString(R.string.bluetooth_searching)),
                statusView.getText().toString());

        mChooserDialog.closeDialog();
    }

    // TODO(jyasskin): Test when the user denies Chrome the ability to ask for permission.

    @Test
    @SmallTest
    public void testTurnOnAdapter() {
        final ItemChooserDialog itemChooser = mChooserDialog.mItemChooserDialog;
        Dialog dialog = itemChooser.getDialogForTesting();
        Assert.assertTrue(dialog.isShowing());

        final TextViewWithClickableSpans statusView = dialog.findViewById(R.id.status);
        final TextViewWithClickableSpans errorView = dialog.findViewById(R.id.not_found_message);
        final View items = dialog.findViewById(R.id.items);
        final Button button = dialog.findViewById(R.id.positive);
        final View progress = dialog.findViewById(R.id.progress);

        final TestAndroidPermissionDelegate permissionDelegate =
                new TestAndroidPermissionDelegate(dialog);
        mWindowAndroid.setAndroidPermissionDelegate(permissionDelegate);

        // Grant permission
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
            permissionDelegate.mBluetoothConnectGranted = true;
            permissionDelegate.mBluetoothScanGranted = true;
        } else {
            permissionDelegate.mLocationGranted = true;
        }

        // Turn off adapter.
        ThreadUtils.runOnUiThreadBlocking(() -> mChooserDialog.notifyAdapterTurnedOff());

        Assert.assertEquals(
                removeLinkTags(
                        sActivityTestRule.getActivity().getString(R.string.bluetooth_adapter_off)),
                errorView.getText().toString());
        Assert.assertEquals(
                removeLinkTags(
                        sActivityTestRule
                                .getActivity()
                                .getString(R.string.bluetooth_adapter_off_help)),
                statusView.getText().toString());
        Assert.assertFalse(button.isEnabled());
        Assert.assertEquals(View.VISIBLE, errorView.getVisibility());
        Assert.assertEquals(View.GONE, items.getVisibility());
        Assert.assertEquals(View.GONE, progress.getVisibility());

        // Turn on adapter.
        ThreadUtils.runOnUiThreadBlocking(() -> itemChooser.signalInitializingAdapter());

        Assert.assertEquals(View.GONE, errorView.getVisibility());
        Assert.assertEquals(View.GONE, items.getVisibility());
        Assert.assertEquals(View.VISIBLE, progress.getVisibility());

        mChooserDialog.closeDialog();
    }

    @Test
    @SmallTest
    @DisabledTest(message = "b/343347280")
    public void testChooserBlockedByModalDialogManager() {
        ModalDialogManager mockModalDialogManager = mock(ModalDialogManager.class);
        when(mockModalDialogManager.isSuspended(ModalDialogManager.ModalDialogType.APP))
                .thenReturn(true);
        when(mockModalDialogManager.isSuspended(ModalDialogManager.ModalDialogType.TAB))
                .thenReturn(true);
        Activity mockActivity = mock(Activity.class);
        WindowAndroid mockWindowAndroid = mock(WindowAndroid.class);
        when(mockWindowAndroid.getActivity()).thenReturn(new WeakReference<>(mockActivity));
        when(mockWindowAndroid.getModalDialogManager()).thenReturn(mockModalDialogManager);
        when(mockWindowAndroid.hasPermission(Manifest.permission.BLUETOOTH_SCAN)).thenReturn(true);
        when(mockWindowAndroid.hasPermission(Manifest.permission.BLUETOOTH_CONNECT))
                .thenReturn(true);
        when(mockWindowAndroid.hasPermission(Manifest.permission.ACCESS_FINE_LOCATION))
                .thenReturn(true);

        BluetoothChooserDialog dialog;
        dialog =
                ThreadUtils.runOnUiThreadBlocking(
                        () -> {
                            return BluetoothChooserDialog.create(
                                    mockWindowAndroid,
                                    "https://origin.example.com/",
                                    ConnectionSecurityLevel.SECURE,
                                    /* delegate= */ null,
                                    /* nativeUsbChooserDialogPtr= */ 42);
                        });
        Assert.assertNull(dialog);
    }

    private static class TestAndroidPermissionDelegate implements AndroidPermissionDelegate {
        Dialog mDialog;
        PermissionCallback mCallback;
        String[] mPermissionsRequested;
        public boolean mLocationGranted;
        public boolean mBluetoothScanGranted;
        public boolean mBluetoothConnectGranted;

        public TestAndroidPermissionDelegate(Dialog dialog) {
            mLocationGranted = false;
            mBluetoothScanGranted = false;
            mBluetoothConnectGranted = false;
            mDialog = dialog;
        }

        @Override
        public boolean hasPermission(String permission) {
            if (permission.equals(Manifest.permission.ACCESS_FINE_LOCATION)) {
                return mLocationGranted;
            } else if (permission.equals(Manifest.permission.BLUETOOTH_SCAN)) {
                return mBluetoothScanGranted;
            } else if (permission.equals(Manifest.permission.BLUETOOTH_CONNECT)) {
                return mBluetoothConnectGranted;
            } else {
                return false;
            }
        }

        @Override
        public boolean canRequestPermission(String permission) {
            return true;
        }

        @Override
        public boolean isPermissionRevokedByPolicy(String permission) {
            return false;
        }

        @Override
        public void requestPermissions(String[] permissions, PermissionCallback callback) {
            // Requesting for permission takes away focus from the window.
            mDialog.onWindowFocusChanged(/* hasFocus= */ false);
            mPermissionsRequested = permissions;
            mCallback = callback;
        }

        @Override
        public boolean handlePermissionResult(
                int requestCode, String[] permissions, int[] grantResults) {
            return false;
        }
    }

    private static class FakeLocationUtils extends LocationUtils {
        public boolean mLocationGranted;

        @Override
        public boolean hasAndroidLocationPermission() {
            return mLocationGranted;
        }

        public boolean mSystemLocationSettingsEnabled = true;

        @Override
        public boolean isSystemLocationSettingEnabled() {
            return mSystemLocationSettingsEnabled;
        }
    }
}