Skip to content

Commit

Permalink
[Bluetooth] Add Passkey prompt dialog for bonding.
Browse files Browse the repository at this point in the history
Add a Bluetooth PIN/Passkey dialog used for prompting the user
during the bonding operation. Only used on platforms which do
not automatically bond: Windows and Linux.

Bug: 960258
Change-Id: I1948be1076bb4a3821791cc8eef080de3b7ac4ae
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2841104
Reviewed-by: Scott Violet <sky@chromium.org>
Reviewed-by: John Abd-El-Malek <jam@chromium.org>
Reviewed-by: Clark DuVall <cduvall@chromium.org>
Reviewed-by: Reilly Grant <reillyg@chromium.org>
Reviewed-by: Ravjit Singh Uppal <ravjit@chromium.org>
Commit-Queue: Chris Mumford <cmumford@google.com>
Cr-Commit-Position: refs/heads/main@{#928399}
  • Loading branch information
cmumford authored and Chromium LUCI CQ committed Oct 5, 2021
1 parent 74876f4 commit 4d1cf30
Show file tree
Hide file tree
Showing 45 changed files with 1,545 additions and 329 deletions.
15 changes: 15 additions & 0 deletions chrome/app/generated_resources.grd
Original file line number Diff line number Diff line change
Expand Up @@ -2774,6 +2774,21 @@ are declared in tools/grit/grit_rule.gni.
</message>
</if>

<!-- Bluetooth Device Credentials (i.e. PIN/Passkey) dialog -->
<if expr="is_win or is_linux">
<message name="IDS_BLUETOOTH_DEVICE_CREDENTIALS_TITLE" desc="Title of the Bluetooth device credentials prompt dialog.">
Device Credentials
</message>
<!--
NOTE: As per Bluetooth v4.2 spec. (section 3.2.3.2):
> When the Bluetooth PIN is referred to on the UI level, the term
> 'Bluetooth Passkey' should be used."
-->
<message name="IDS_BLUETOOTH_DEVICE_CREDENTIALS_LABEL" desc="Label of the text prompt for Bluetooth device credentials.">
Enter Bluetooth Passkey for device <ph name="DEVICE">$1</ph>
</message>
</if>

<!-- Content blocking strings -->
<message name="IDS_MANAGE" desc="Text for a button on permission bubbles, which opens a more detailed content settings page where users can manage that particular setting.">
Manage
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
752279e3f4c6d9e32ba7c88ea5e94ce4d3a8acd4
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
2978c1a64b5ab43841d322f97b4f1d50e0851e64
5 changes: 5 additions & 0 deletions chrome/browser/about_flags.cc
Original file line number Diff line number Diff line change
Expand Up @@ -7585,6 +7585,11 @@ const FeatureEntry kFeatureEntries[] = {
flag_descriptions::kCSSCascadeLayersDescription, kOsAll,
FEATURE_VALUE_TYPE(blink::features::kCSSCascadeLayers)},

{"bluetooth-bond-on-demand",
flag_descriptions::kWebBluetoothBondOnDemandName,
flag_descriptions::kWebBluetoothBondOnDemandDescription, kOsWin | kOsLinux,
FEATURE_VALUE_TYPE(features::kWebBluetoothBondOnDemand)},

// NOTE: Adding a new flag requires adding a corresponding entry to enum
// "LoginCustomFlags" in tools/metrics/histograms/enums.xml. See "Flag
// Histograms" in tools/metrics/histograms/README.md (run the
Expand Down
19 changes: 19 additions & 0 deletions chrome/browser/bluetooth/chrome_bluetooth_delegate_impl_client.cc
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,22 @@ ChromeBluetoothDelegateImplClient::ShowBluetoothScanningPrompt(
base::BindOnce(chrome::ShowDeviceChooserDialog, frame));
#endif
}

void ChromeBluetoothDelegateImplClient::ShowBluetoothDeviceCredentialsDialog(
content::RenderFrameHost* frame,
const std::u16string& device_identifier,
content::BluetoothDelegate::CredentialsCallback callback) {
#if PAIR_BLUETOOTH_ON_DEMAND()
chrome::ShowBluetoothDeviceCredentialsDialog(
content::WebContents::FromRenderFrameHost(frame), device_identifier,
std::move(callback));
#else
// WebBluetoothServiceImpl will only start the pairing process (which prompts
// for credentials) on devices that pair on demand. This should never be
// reached.
NOTREACHED();
std::move(callback).Run(
content::BluetoothDelegate::DeviceCredentialsPromptResult::kCancelled,
u"");
#endif
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ class ChromeBluetoothDelegateImplClient
content::RenderFrameHost* frame,
const content::BluetoothScanningPrompt::EventHandler& event_handler)
override;
void ShowBluetoothDeviceCredentialsDialog(
content::RenderFrameHost* frame,
const std::u16string& device_identifier,
content::BluetoothDelegate::CredentialsCallback callback) override;
};

#endif // CHROME_BROWSER_BLUETOOTH_CHROME_BLUETOOTH_DELEGATE_IMPL_CLIENT_H_
5 changes: 5 additions & 0 deletions chrome/browser/flag-metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,11 @@
"owners": [ "cvandermerwe@google.com", "hansenmichael@google.com", "chromeos-cross-device-eng@google.com" ],
"expiry_milestone": 97
},
{
"name": "bluetooth-bond-on-demand",
"owners": [ "cmumford@chromium.org", "deviceapi-team@google.com" ],
"expiry_milestone": 100
},
{
"name": "bluetooth-fix-a2dp-packet-size",
"owners": [ "michaelfsun", "chromeos-bluetooth@google.com" ],
Expand Down
6 changes: 6 additions & 0 deletions chrome/browser/flag_descriptions.cc
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,12 @@ const char kEnableBluetoothSerialPortProfileInSerialApiDescription[] =
"When enabled, Bluetooth Serial Port Profile devices will be enumerated "
"for use with the Serial API.";

const char kWebBluetoothBondOnDemandName[] =
"Enable on-demand Bluetooth device bonding";
const char kWebBluetoothBondOnDemandDescription[] =
"When enabled, Bluetooth will start the bonding process, if necessary, "
"to access protected characteristics.";

const char kEnableDrDcName[] =
"Enables Display Compositor to use a new gpu thread.";
const char kEnableDrDcDescription[] =
Expand Down
3 changes: 3 additions & 0 deletions chrome/browser/flag_descriptions.h
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,9 @@ extern const char kEditPasswordsInSettingsDescription[];
extern const char kEnableBluetoothSerialPortProfileInSerialApiName[];
extern const char kEnableBluetoothSerialPortProfileInSerialApiDescription[];

extern const char kWebBluetoothBondOnDemandName[];
extern const char kWebBluetoothBondOnDemandDescription[];

extern const char kEnableDrDcName[];
extern const char kEnableDrDcDescription[];

Expand Down
7 changes: 7 additions & 0 deletions chrome/browser/ui/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -4716,6 +4716,13 @@ static_library("ui") {
]
}

if (is_win || is_linux) {
sources += [
"views/bluetooth_device_credentials_view.cc",
"views/bluetooth_device_credentials_view.h",
]
}

if (is_mac) {
sources += [
"views/apps/app_window_native_widget_mac.h",
Expand Down
13 changes: 13 additions & 0 deletions chrome/browser/ui/browser_dialogs.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#include "chrome/browser/web_applications/web_app_callback_app_identity.h"
#include "chrome/browser/web_applications/web_app_id.h"
#include "chrome/common/buildflags.h"
#include "content/public/browser/bluetooth_delegate.h"
#include "content/public/browser/login_delegate.h"
#include "extensions/buildflags/buildflags.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
Expand Down Expand Up @@ -119,6 +120,17 @@ void ShowCreateChromeAppShortcutsDialog(
const std::string& web_app_id,
base::OnceCallback<void(bool /* created */)> close_callback);

#if PAIR_BLUETOOTH_ON_DEMAND()
// Shows the dialog to request the Bluetooth credentials for the device
// identified by |device_identifier|. |device_identifier| is the most
// appropriate string to display to the user for device identification
// (e.g. name, MAC address).
void ShowBluetoothDeviceCredentialsDialog(
content::WebContents* web_contents,
const std::u16string& device_identifier,
content::BluetoothDelegate::CredentialsCallback close_callback);
#endif // PAIR_BLUETOOTH_ON_DEMAND()

// Callback used to indicate whether a user has accepted the installation of a
// web app. The boolean parameter is true when the user accepts the dialog. The
// WebApplicationInfo parameter contains the information about the app,
Expand Down Expand Up @@ -379,6 +391,7 @@ enum class DialogIdentifier {
FILE_HANDLING_PERMISSION_REQUEST = 109,
SIGNIN_ENTERPRISE_INTERCEPTION = 110,
APP_IDENTITY_UPDATE_CONFIRMATION = 111,
BLUETOOTH_DEVICE_CREDENTIALS = 112,
// Add values above this line with a corresponding label in
// tools/metrics/histograms/enums.xml
MAX_VALUE
Expand Down
203 changes: 203 additions & 0 deletions chrome/browser/ui/views/bluetooth_device_credentials_view.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
// Copyright 2021 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.

#include "chrome/browser/ui/views/bluetooth_device_credentials_view.h"

#include <cwctype>

#include "base/bind.h"
#include "base/callback_helpers.h"
#include "base/ranges/algorithm.h"
#include "chrome/browser/ui/browser_dialogs.h"
#include "chrome/browser/ui/views/chrome_layout_provider.h"
#include "chrome/grit/generated_resources.h"
#include "components/constrained_window/constrained_window_views.h"
#include "components/vector_icons/vector_icons.h"
#include "device/bluetooth/strings/grit/bluetooth_strings.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/compositor/layer.h"
#include "ui/gfx/color_utils.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/label.h"
#include "ui/views/layout/flex_layout.h"

using content::BluetoothDelegate;

namespace chrome {

void ShowBluetoothDeviceCredentialsDialog(
content::WebContents* web_contents,
const std::u16string& device_identifier,
BluetoothDelegate::CredentialsCallback close_callback) {
// This dialog owns itself. DialogDelegateView will delete |dialog| instance.
auto* dialog = new BluetoothDeviceCredentialsView(device_identifier,
std::move(close_callback));
constrained_window::ShowWebModalDialogViews(dialog, web_contents);
}

} // namespace chrome

namespace {

bool IsInputTextValid(const std::u16string& text) {
const size_t num_digits =
base::ranges::count_if(text, [](auto ch) { return std::iswdigit(ch); });
// This dialog is currently only used to prompt for Bluetooth PINs which
// are always six digit numeric values as per the spec. This function could
// do a better job of validating input, but should also be accompanied by
// a better UI to help the user understand why the "OK" button is disabled
// when a seemingly valid PIN, which doesn't conform to the spec., has been
// input.
return num_digits > 0;
}

} // namespace

BluetoothDeviceCredentialsView::BluetoothDeviceCredentialsView(
const std::u16string& device_identifier,
BluetoothDelegate::CredentialsCallback close_callback)
: close_callback_(std::move(close_callback)) {
SetModalType(ui::MODAL_TYPE_CHILD);
set_margins(ChromeLayoutProvider::Get()->GetDialogInsetsForContentType(
views::DialogContentType::kText, views::DialogContentType::kText));
SetAcceptCallback(
base::BindOnce(&BluetoothDeviceCredentialsView::OnDialogAccepted,
base::Unretained(this)));
auto canceled = [](BluetoothDeviceCredentialsView* dialog) {
std::move(dialog->close_callback_)
.Run(BluetoothDelegate::DeviceCredentialsPromptResult::kCancelled, u"");
};
SetCancelCallback(base::BindOnce(canceled, base::Unretained(this)));
SetCloseCallback(base::BindOnce(canceled, base::Unretained(this)));
InitControls(device_identifier);

chrome::RecordDialogCreation(
chrome::DialogIdentifier::BLUETOOTH_DEVICE_CREDENTIALS);
}

BluetoothDeviceCredentialsView::~BluetoothDeviceCredentialsView() = default;

void BluetoothDeviceCredentialsView::InitControls(
const std::u16string& device_identifier) {
//
// Create the following layout:
//
// ┌───────────────┬─────────────────────────────────────────────┐
// │ │ Device passkey │
// │ ┌───────────┐ │ │
// │ │ │ │ Please enter the passkey for <device name>: │
// │ │ Bluetooth │ │ ┌────────────────────────────────────────┐ │
// │ │ icon │ │ │ │ │
// │ │ │ │ └────────────────────────────────────────┘ │
// │ └───────────┘ │ ┌──────┐ ┌────────┐ │
// │ │ │ OK │ │ Cancel │ │
// │ │ └──────┘ └────────┘ │
// └───────────────┴─────────────────────────────────────────────┘
//

SetLayoutManager(std::make_unique<views::FlexLayout>())
->SetCrossAxisAlignment(views::LayoutAlignment::kCenter);

// The vertical space that must exist on the top and the bottom of the item
// to ensure the proper spacing is maintained between items when stacking
// vertically.
const int vertical_spacing = ChromeLayoutProvider::Get()->GetDistanceMetric(
DISTANCE_CONTROL_LIST_VERTICAL) /
2;
constexpr int horizontal_spacing = 0;

constexpr int kIconSize = 48; // width and height.
auto icon_view = std::make_unique<views::ImageView>();
icon_view->SetImage(gfx::CreateVectorIcon(
vector_icons::kBluetoothIcon, kIconSize,
color_utils::DeriveDefaultIconColor(gfx::kGoogleGrey700)));
icon_view_ = AddChildView(std::move(icon_view));

auto contents_wrapper = std::make_unique<views::View>();
contents_wrapper->SetProperty(
views::kMarginsKey, gfx::Insets(vertical_spacing, horizontal_spacing));

contents_wrapper->SetLayoutManager(std::make_unique<views::FlexLayout>())
->SetOrientation(views::LayoutOrientation::kVertical)
.SetMainAxisAlignment(views::LayoutAlignment::kCenter);
contents_wrapper->SetProperty(
views::kFlexBehaviorKey,
views::FlexSpecification(views::MinimumFlexSizeRule::kScaleToZero,
views::MaximumFlexSizeRule::kUnbounded));

views::Label* passkey_prompt_label_ptr = nullptr;
{
auto passkey_prompt_label =
std::make_unique<views::Label>(l10n_util::GetStringFUTF16(
IDS_BLUETOOTH_DEVICE_CREDENTIALS_LABEL, device_identifier));
passkey_prompt_label_ptr = passkey_prompt_label.get();
passkey_prompt_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
passkey_prompt_label->SetMultiLine(true);
contents_wrapper->AddChildView(std::move(passkey_prompt_label));
}

{
constexpr int kDefaultTextfieldNumChars = 8;
constexpr int kMinimumTextfieldNumChars = 6;

passkey_text_ =
contents_wrapper->AddChildView(std::make_unique<views::Textfield>());
passkey_text_->set_controller(this);
passkey_text_->SetDefaultWidthInChars(kDefaultTextfieldNumChars);
passkey_text_->SetMinimumWidthInChars(kMinimumTextfieldNumChars);
passkey_text_->SetTextInputType(ui::TEXT_INPUT_TYPE_TEXT);
passkey_text_->SetAssociatedLabel(passkey_prompt_label_ptr);
// TODO(cmumford): Windows Narrator says "no item in view".
}

contents_wrapper_ = AddChildView(std::move(contents_wrapper));
}

views::View* BluetoothDeviceCredentialsView::GetInitiallyFocusedView() {
return passkey_text_;
}

gfx::Size BluetoothDeviceCredentialsView::CalculatePreferredSize() const {
constexpr int kDialogWidth = 360;
int height =
GetLayoutManager()->GetPreferredHeightForWidth(this, kDialogWidth);
return gfx::Size(kDialogWidth, height);
}

bool BluetoothDeviceCredentialsView::IsDialogButtonEnabled(
ui::DialogButton button) const {
if (button != ui::DIALOG_BUTTON_OK)
return true; // Only "OK" button is sensitized - all others are enabled.

return IsInputTextValid(passkey_text_->GetText());
}

std::u16string BluetoothDeviceCredentialsView::GetWindowTitle() const {
return l10n_util::GetStringUTF16(IDS_BLUETOOTH_DEVICE_CREDENTIALS_TITLE);
}

void BluetoothDeviceCredentialsView::OnDialogAccepted() {
DCHECK(IsDialogButtonEnabled(ui::DIALOG_BUTTON_OK));

std::u16string trimmed_input;
base::TrimWhitespace(passkey_text_->GetText(), base::TRIM_ALL,
&trimmed_input);

std::move(close_callback_)
.Run(BluetoothDelegate::DeviceCredentialsPromptResult::kSuccess,
std::move(trimmed_input));
}

void BluetoothDeviceCredentialsView::ContentsChanged(
views::Textfield* sender,
const std::u16string& new_contents) {
DCHECK_EQ(sender, passkey_text_);
SetButtonEnabled(ui::DIALOG_BUTTON_OK, IsInputTextValid(new_contents));
DialogModelChanged();
}

BEGIN_METADATA(BluetoothDeviceCredentialsView, views::DialogDelegateView)
END_METADATA
Loading

0 comments on commit 4d1cf30

Please sign in to comment.