blob: 46bf83fe490aec573188f58fb14ccece059bf18d [file] [log] [blame]
// Copyright 2016 The Chromium Authors. All rights reserved.
// 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;
import android.Manifest;
import android.app.Dialog;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.location.LocationManager;
import android.support.test.filters.LargeTest;
import android.view.View;
import android.widget.Button;
import android.widget.ListView;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.chromium.base.ThreadUtils;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.RetryOnFailure;
import org.chromium.chrome.R;
import org.chromium.chrome.test.ChromeActivityTestRule;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.components.location.LocationUtils;
import org.chromium.components.security_state.ConnectionSecurityLevel;
import org.chromium.content.browser.test.util.Criteria;
import org.chromium.content.browser.test.util.CriteriaHelper;
import org.chromium.content.browser.test.util.TouchCommon;
import org.chromium.ui.base.ActivityWindowAndroid;
import org.chromium.ui.base.AndroidPermissionDelegate;
import org.chromium.ui.base.PermissionCallback;
import org.chromium.ui.base.WindowAndroid;
import org.chromium.ui.widget.TextViewWithClickableSpans;
/**
* Tests for the BluetoothChooserDialog class.
*/
@RunWith(ChromeJUnit4ClassRunner.class)
@RetryOnFailure
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
public class BluetoothChooserDialogTest {
/**
* Works like the BluetoothChooserDialog class, but records calls to native methods instead of
* calling back to C++.
*/
static class BluetoothChooserDialogWithFakeNatives extends BluetoothChooserDialog {
int mFinishedEventType = -1;
String mFinishedDeviceId;
int mRestartSearchCount = 0;
BluetoothChooserDialogWithFakeNatives(WindowAndroid windowAndroid, String origin,
int securityLevel, long nativeBluetoothChooserDialogPtr) {
super(windowAndroid, origin, securityLevel, nativeBluetoothChooserDialogPtr);
}
@Override
void nativeOnDialogFinished(
long nativeBluetoothChooserAndroid, int eventType, String deviceId) {
Assert.assertEquals(nativeBluetoothChooserAndroid, mNativeBluetoothChooserDialogPtr);
Assert.assertEquals(mFinishedEventType, -1);
mFinishedEventType = eventType;
mFinishedDeviceId = deviceId;
// The native code calls closeDialog() when OnDialogFinished is called.
closeDialog();
}
@Override
void nativeRestartSearch(long nativeBluetoothChooserAndroid) {
Assert.assertTrue(mNativeBluetoothChooserDialogPtr != 0);
mRestartSearchCount++;
}
@Override
void nativeShowBluetoothOverviewLink(long nativeBluetoothChooserAndroid) {
// We shouldn't be running native functions if the native class has been destroyed.
Assert.assertTrue(mNativeBluetoothChooserDialogPtr != 0);
}
@Override
void nativeShowBluetoothAdapterOffLink(long nativeBluetoothChooserAndroid) {
// We shouldn't be running native functions if the native class has been destroyed.
Assert.assertTrue(mNativeBluetoothChooserDialogPtr != 0);
}
@Override
void nativeShowNeedLocationPermissionLink(long nativeBluetoothChooserAndroid) {
// We shouldn't be running native functions if the native class has been destroyed.
Assert.assertTrue(mNativeBluetoothChooserDialogPtr != 0);
}
}
private ActivityWindowAndroid mWindowAndroid;
private FakeLocationUtils mLocationUtils;
private BluetoothChooserDialogWithFakeNatives mChooserDialog;
@Rule
public ChromeActivityTestRule<ChromeActivity> mActivityTestRule =
new ChromeActivityTestRule<>(ChromeActivity.class);
@Before
public void setUp() throws Exception {
mActivityTestRule.startMainActivityOnBlankPage();
mLocationUtils = new FakeLocationUtils();
LocationUtils.setFactory(() -> mLocationUtils);
mChooserDialog = createDialog();
}
@After
public void tearDown() throws Exception {
LocationUtils.setFactory(null);
}
private BluetoothChooserDialogWithFakeNatives createDialog() {
return ThreadUtils.runOnUiThreadBlockingNoException(
() -> {
mWindowAndroid = new ActivityWindowAndroid(mActivityTestRule.getActivity());
BluetoothChooserDialogWithFakeNatives dialog =
new BluetoothChooserDialogWithFakeNatives(mWindowAndroid,
"https://origin.example.com/",
ConnectionSecurityLevel.SECURE, 42);
dialog.show();
return dialog;
});
}
private static void selectItem(final BluetoothChooserDialogWithFakeNatives chooserDialog,
int position) {
final Dialog dialog = chooserDialog.mItemChooserDialog.getDialogForTesting();
final ListView items = (ListView) dialog.findViewById(R.id.items);
final Button button = (Button) dialog.findViewById(R.id.positive);
CriteriaHelper.pollUiThread(new Criteria() {
@Override
public boolean isSatisfied() {
return items.getChildAt(0) != null;
}
});
Assert.assertEquals("Not all items have a view; positions may be incorrect.",
items.getChildCount(), items.getAdapter().getCount());
// Verify first item selected gets selected.
TouchCommon.singleClickView(items.getChildAt(position - 1));
CriteriaHelper.pollUiThread(new Criteria() {
@Override
public boolean isSatisfied() {
return button.isEnabled();
}
});
TouchCommon.singleClickView(button);
CriteriaHelper.pollUiThread(new Criteria() {
@Override
public boolean isSatisfied() {
return chooserDialog.mFinishedEventType != -1;
}
});
}
/**
* 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[^>]*>", "");
}
@Test
@LargeTest
public void testCancel() {
ItemChooserDialog itemChooser = mChooserDialog.mItemChooserDialog;
Dialog dialog = itemChooser.getDialogForTesting();
Assert.assertTrue(dialog.isShowing());
TextViewWithClickableSpans statusView =
(TextViewWithClickableSpans) dialog.findViewById(R.id.status);
final ListView items = (ListView) dialog.findViewById(R.id.items);
final Button 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(mActivityTestRule.getActivity().getString(
R.string.bluetooth_searching)),
statusView.getText().toString());
Assert.assertFalse(button.isEnabled());
Assert.assertEquals(View.GONE, items.getVisibility());
dialog.dismiss();
CriteriaHelper.pollUiThread(new Criteria() {
@Override
public boolean isSatisfied() {
return mChooserDialog.mFinishedEventType != -1;
}
});
Assert.assertEquals(
BluetoothChooserDialog.DialogFinished.CANCELLED, mChooserDialog.mFinishedEventType);
Assert.assertEquals("", mChooserDialog.mFinishedDeviceId);
}
@Test
@LargeTest
public void testSelectItem() {
Dialog dialog = mChooserDialog.mItemChooserDialog.getDialogForTesting();
TextViewWithClickableSpans statusView =
(TextViewWithClickableSpans) dialog.findViewById(R.id.status);
final View items = dialog.findViewById(R.id.items);
final Button 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", false /* isGATTConnected */,
-1 /* signalStrengthLevel */);
// Add connected device with no signal strength.
mChooserDialog.addOrUpdateDevice(
"id-2", "Name 2", true /* isGATTConnected */, -1 /* signalStrengthLevel */);
// Add non-connected device with signal strength level 1.
mChooserDialog.addOrUpdateDevice(
"id-3", "Name 3", false /* isGATTConnected */, 1 /* signalStrengthLevel */);
// Add connected device with signal strength level 1.
mChooserDialog.addOrUpdateDevice(
"id-4", "Name 4", true /* isGATTConnected */, 1 /* signalStrengthLevel */);
});
// 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(mActivityTestRule.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());
ItemChooserDialog.ItemAdapter itemAdapter =
mChooserDialog.mItemChooserDialog.getItemAdapterForTesting();
Assert.assertTrue(itemAdapter.getItem(0).hasSameContents(
"id-1", "Name 1", null /* icon */, null /* iconDescription */));
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],
mActivityTestRule.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(mChooserDialog, 2);
Assert.assertEquals(
BluetoothChooserDialog.DialogFinished.SELECTED, mChooserDialog.mFinishedEventType);
Assert.assertEquals("id-2", mChooserDialog.mFinishedDeviceId);
}
@Test
@LargeTest
public void testNoLocationPermission() {
ItemChooserDialog itemChooser = mChooserDialog.mItemChooserDialog;
Dialog dialog = itemChooser.getDialogForTesting();
Assert.assertTrue(dialog.isShowing());
final TextViewWithClickableSpans statusView =
(TextViewWithClickableSpans) dialog.findViewById(R.id.status);
final TextViewWithClickableSpans errorView =
(TextViewWithClickableSpans) dialog.findViewById(R.id.not_found_message);
final View items = dialog.findViewById(R.id.items);
final Button 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));
Assert.assertEquals(removeLinkTags(mActivityTestRule.getActivity().getString(
R.string.bluetooth_need_location_permission)),
errorView.getText().toString());
Assert.assertEquals(removeLinkTags(mActivityTestRule.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.assertArrayEquals(permissionDelegate.mPermissionsRequested,
new String[] {Manifest.permission.ACCESS_COARSE_LOCATION});
Assert.assertNotNull(permissionDelegate.mCallback);
// Grant permission.
mLocationUtils.mLocationGranted = true;
ThreadUtils.runOnUiThreadBlocking(
() -> permissionDelegate.mCallback.onRequestPermissionsResult(
new String[]{Manifest.permission.ACCESS_COARSE_LOCATION},
new int[]{PackageManager.PERMISSION_GRANTED}));
Assert.assertEquals(1, mChooserDialog.mRestartSearchCount);
Assert.assertEquals(removeLinkTags(mActivityTestRule.getActivity().getString(
R.string.bluetooth_searching)),
statusView.getText().toString());
mChooserDialog.closeDialog();
}
@Test
@LargeTest
public void testNoLocationServices() {
ItemChooserDialog itemChooser = mChooserDialog.mItemChooserDialog;
Dialog dialog = itemChooser.getDialogForTesting();
Assert.assertTrue(dialog.isShowing());
final TextViewWithClickableSpans statusView =
(TextViewWithClickableSpans) dialog.findViewById(R.id.status);
final TextViewWithClickableSpans errorView =
(TextViewWithClickableSpans) dialog.findViewById(R.id.not_found_message);
final View items = dialog.findViewById(R.id.items);
final Button 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.
mLocationUtils.mLocationGranted = true;
mLocationUtils.mSystemLocationSettingsEnabled = false;
ThreadUtils.runOnUiThreadBlocking(
()
-> mChooserDialog.notifyDiscoveryState(
BluetoothChooserDialog.DiscoveryMode.DISCOVERY_FAILED_TO_START));
Assert.assertEquals(removeLinkTags(mActivityTestRule.getActivity().getString(
R.string.bluetooth_need_location_services_on)),
errorView.getText().toString());
Assert.assertEquals(removeLinkTags(mActivityTestRule.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(
mActivityTestRule.getActivity(),
new Intent(LocationManager.MODE_CHANGED_ACTION)));
Assert.assertEquals(1, mChooserDialog.mRestartSearchCount);
Assert.assertEquals(removeLinkTags(mActivityTestRule.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
@LargeTest
public void testTurnOnAdapter() {
final ItemChooserDialog itemChooser = mChooserDialog.mItemChooserDialog;
Dialog dialog = itemChooser.getDialogForTesting();
Assert.assertTrue(dialog.isShowing());
final TextViewWithClickableSpans statusView =
(TextViewWithClickableSpans) dialog.findViewById(R.id.status);
final TextViewWithClickableSpans errorView =
(TextViewWithClickableSpans) dialog.findViewById(R.id.not_found_message);
final View items = dialog.findViewById(R.id.items);
final Button button = (Button) dialog.findViewById(R.id.positive);
final View progress = dialog.findViewById(R.id.progress);
// Turn off adapter.
ThreadUtils.runOnUiThreadBlocking(() -> mChooserDialog.notifyAdapterTurnedOff());
Assert.assertEquals(removeLinkTags(mActivityTestRule.getActivity().getString(
R.string.bluetooth_adapter_off)),
errorView.getText().toString());
Assert.assertEquals(removeLinkTags(mActivityTestRule.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();
}
private static class TestAndroidPermissionDelegate implements AndroidPermissionDelegate {
Dialog mDialog = null;
PermissionCallback mCallback = null;
String[] mPermissionsRequested = null;
public TestAndroidPermissionDelegate(Dialog dialog) {
mDialog = dialog;
}
@Override
public boolean hasPermission(String permission) {
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(false /* hasFocus */);
mPermissionsRequested = permissions;
if (permissions.length == 1
&& permissions[0].equals(Manifest.permission.ACCESS_COARSE_LOCATION)) {
mCallback = callback;
}
}
@Override
public boolean handlePermissionResult(
int requestCode, String[] permissions, int[] grantResults) {
return false;
}
}
private static class FakeLocationUtils extends LocationUtils {
public boolean mLocationGranted = false;
@Override
public boolean hasAndroidLocationPermission() {
return mLocationGranted;
}
public boolean mSystemLocationSettingsEnabled = true;
@Override
public boolean isSystemLocationSettingEnabled() {
return mSystemLocationSettingsEnabled;
}
}
}