[go: nahoru, domu]

blob: 7027f96a9602d2bc6f9c5142467fdebaace84a47 [file] [log] [blame]
// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "device/bluetooth/chromeos/bluetooth_utils.h"
#include "base/containers/contains.h"
#include "base/containers/fixed_flat_set.h"
#include "base/feature_list.h"
#include "base/metrics/field_trial_params.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/time/time.h"
#include "build/chromeos_buildflags.h"
#include "chromeos/constants/chromeos_features.h"
#include "device/base/features.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
#if BUILDFLAG(IS_CHROMEOS_ASH)
#include <string_view>
#include "ash/constants/ash_features.h"
#include "ash/constants/ash_switches.h"
#include "chromeos/ash/services/nearby/public/cpp/nearby_client_uuids.h"
#include "chromeos/ash/services/secure_channel/public/cpp/shared/ble_constants.h"
#endif // BUILDFLAG(IS_CHROMEOS_ASH)
#if BUILDFLAG(IS_CHROMEOS_LACROS)
#include "chromeos/startup/browser_params_proxy.h"
#endif // BUILDFLAG(IS_CHROMEOS_LACROS)
namespace device {
namespace {
// https://www.bluetooth.com/specifications/gatt/services.
const char kHIDServiceUUID[] = "1812";
// https://www.bluetooth.com/specifications/assigned-numbers/16-bit-uuids-for-sdos.
const char kSecurityKeyServiceUUID[] = "FFFD";
constexpr base::TimeDelta kMaxDeviceSelectionDuration = base::Seconds(30);
constexpr uint8_t kLimitedDiscoveryFlag = 0x01;
constexpr uint8_t kGeneralDiscoveryFlag = 0x02;
// Get limited number of devices from |devices| and
// prioritize paired/connecting devices over other devices.
BluetoothAdapter::DeviceList GetLimitedNumDevices(
size_t max_device_num,
const BluetoothAdapter::DeviceList& devices) {
// If |max_device_num| is 0, it means there's no limit.
if (max_device_num == 0)
return devices;
BluetoothAdapter::DeviceList result;
for (BluetoothDevice* device : devices) {
if (result.size() == max_device_num)
break;
if (device->IsPaired() || device->IsConnecting())
result.push_back(device);
}
for (BluetoothDevice* device : devices) {
if (result.size() == max_device_num)
break;
if (!device->IsPaired() && !device->IsConnecting())
result.push_back(device);
}
return result;
}
// Filter out unknown devices from the list.
BluetoothAdapter::DeviceList FilterUnknownDevices(
const BluetoothAdapter::DeviceList& devices) {
#if BUILDFLAG(IS_CHROMEOS_ASH)
if (ash::switches::IsUnfilteredBluetoothDevicesEnabled())
return devices;
#endif // BUILDFLAG(IS_CHROMEOS_ASH)
#if BUILDFLAG(IS_CHROMEOS_LACROS)
if (chromeos::BrowserParamsProxy::Get()
->IsUnfilteredBluetoothDeviceEnabled()) {
return devices;
}
#endif // BUILDFLAG(IS_CHROMEOS_LACROS)
BluetoothAdapter::DeviceList result;
for (BluetoothDevice* device : devices) {
if (device::IsUnsupportedDevice(device))
continue;
result.push_back(device);
}
return result;
}
void RecordPairingDuration(const std::string& histogram_name,
base::TimeDelta pairing_duration) {
base::UmaHistogramCustomTimes(histogram_name, pairing_duration,
base::Milliseconds(1) /* min */,
base::Seconds(30) /* max */, 50 /* buckets */);
}
void RecordPairingTransport(BluetoothTransport transport) {
BluetoothTransportType type;
switch (transport) {
case BLUETOOTH_TRANSPORT_CLASSIC:
type = BluetoothTransportType::kClassic;
break;
case BLUETOOTH_TRANSPORT_LE:
type = BluetoothTransportType::kLE;
break;
case BLUETOOTH_TRANSPORT_DUAL:
type = BluetoothTransportType::kDual;
break;
case BLUETOOTH_TRANSPORT_INVALID:
type = BluetoothTransportType::kInvalid;
break;
default:
type = BluetoothTransportType::kUnknown;
break;
}
base::UmaHistogramEnumeration("Bluetooth.ChromeOS.Pairing.TransportType",
type);
}
void RecordDeviceSelectionDuration(const std::string& histogram_name,
base::TimeDelta duration) {
base::UmaHistogramCustomTimes(
histogram_name, duration, base::Milliseconds(1) /* min */,
kMaxDeviceSelectionDuration /* max */, 50 /* buckets */);
}
std::string GetTransportName(BluetoothTransport transport) {
switch (transport) {
case BluetoothTransport::BLUETOOTH_TRANSPORT_CLASSIC:
return "Classic";
case BluetoothTransport::BLUETOOTH_TRANSPORT_LE:
return "BLE";
case BluetoothTransport::BLUETOOTH_TRANSPORT_DUAL:
return "Dual";
case BLUETOOTH_TRANSPORT_INVALID:
return "Invalid";
default:
// A transport type of other is unexpected, and no success
// metric for it exists.
return "";
}
}
void EmitFilteredFailureReason(ConnectionFailureReason failure_reason,
const std::string& transport_name) {
switch (failure_reason) {
case ConnectionFailureReason::kAuthCanceled:
[[fallthrough]];
case ConnectionFailureReason::kAuthRejected:
return;
case ConnectionFailureReason::kUnknownError:
[[fallthrough]];
case ConnectionFailureReason::kAuthFailed:
[[fallthrough]];
case ConnectionFailureReason::kAuthTimeout:
[[fallthrough]];
case ConnectionFailureReason::kUnknownConnectionError:
[[fallthrough]];
case ConnectionFailureReason::kUnsupportedDevice:
[[fallthrough]];
case ConnectionFailureReason::kNotConnectable:
[[fallthrough]];
case ConnectionFailureReason::kSystemError:
[[fallthrough]];
case ConnectionFailureReason::kFailed:
[[fallthrough]];
case ConnectionFailureReason::kInprogress:
const std::string result_histogram_name_prefix =
"Bluetooth.ChromeOS.Pairing.Result";
base::UmaHistogramEnumeration(
result_histogram_name_prefix + ".FilteredFailureReason",
failure_reason);
base::UmaHistogramEnumeration(result_histogram_name_prefix +
".FilteredFailureReason." +
transport_name,
failure_reason);
return;
}
NOTREACHED();
}
#if BUILDFLAG(IS_CHROMEOS_ASH)
bool IsPolyDevice(const device::BluetoothDevice* device) {
// OUI portions of Bluetooth addresses for devices manufactured by Poly. See
// https://standards-oui.ieee.org/.
constexpr auto kPolyOuis = base::MakeFixedFlatSet<std::string_view>(
{"64:16:7F", "48:25:67", "00:04:F2"});
return base::Contains(kPolyOuis, device->GetOuiPortionOfBluetoothAddress());
}
#endif
// Provide heuristics for which transport to use for a dual device
BluetoothTransport InferDeviceTransport(const device::BluetoothDevice* device) {
if (device->GetType() != BLUETOOTH_TRANSPORT_DUAL) {
return device->GetType();
}
#if BUILDFLAG(IS_CHROMEOS)
// Random address type indicates LE device.
if (device->GetAddressType() ==
BluetoothDevice::AddressType::ADDR_TYPE_RANDOM) {
return BLUETOOTH_TRANSPORT_LE;
}
#endif
// Devices without type/appearance most likely signals that it is truly only
// a LE advertisement for a peripheral which is active, but not pairable. Many
// popular headphones behave in this exact way. Mark as invalid until they
// provide a type/appearance; this means they've become pairable. See
// https://crrev.com/c/1656971 for more.
if (device->GetDeviceType() == BluetoothDeviceType::UNKNOWN) {
return BLUETOOTH_TRANSPORT_INVALID;
}
return BLUETOOTH_TRANSPORT_CLASSIC;
}
} // namespace
device::BluetoothAdapter::DeviceList FilterBluetoothDeviceList(
const BluetoothAdapter::DeviceList& devices,
BluetoothFilterType filter_type,
int max_devices) {
BluetoothAdapter::DeviceList filtered_devices =
filter_type == BluetoothFilterType::KNOWN ? FilterUnknownDevices(devices)
: devices;
return GetLimitedNumDevices(max_devices, filtered_devices);
}
bool IsUnsupportedDevice(const device::BluetoothDevice* device) {
#if BUILDFLAG(IS_CHROMEOS_ASH)
if (ash::switches::IsUnfilteredBluetoothDevicesEnabled()) {
return false;
}
#endif // BUILDFLAG(IS_CHROMEOS_ASH)
#if BUILDFLAG(IS_CHROMEOS_LACROS)
if (chromeos::BrowserParamsProxy::Get()
->IsUnfilteredBluetoothDeviceEnabled()) {
return false;
}
#endif // BUILDFLAG(IS_CHROMEOS_LACROS)
#if BUILDFLAG(IS_CHROMEOS_ASH)
// Never filter out Poly devices; this requires a special case since these
// devices often identify themselves as phones, which are disallowed below.
// See b/228118615.
if (IsPolyDevice(device)) {
return false;
}
#endif
#if BUILDFLAG(IS_CHROMEOS)
// Always allow bonded devices to appear in the UI.
if (device->IsBonded()) {
return false;
}
#endif
// Always filter out laptops, etc. There is no intended use case or
// Bluetooth profile in this context.
if (device->GetDeviceType() == BluetoothDeviceType::COMPUTER) {
return true;
}
// Always filter out phones. There is no intended use case or Bluetooth
// profile in this context.
if (base::FeatureList::IsEnabled(chromeos::features::kBluetoothPhoneFilter) &&
device->GetDeviceType() == BluetoothDeviceType::PHONE) {
return true;
}
#if BUILDFLAG(IS_CHROMEOS_ASH)
const BluetoothDevice::UUIDSet& uuids = device->GetUUIDs();
// These UUIDs are specific to Nearby Share and Phone Hub and are used to
// identify devices that should be filtered from the UI that otherwise would
// not have been correctly identified. These devices should always be filtered
// from the UI. For more information see b/219627324.
for (const auto& uuid : ash::nearby::GetNearbyClientUuids()) {
if (uuids.contains(uuid)) {
return true;
}
}
if (uuids.contains(BluetoothUUID(ash::secure_channel::kGattServerUuid))) {
return true;
}
#endif
#if !BUILDFLAG(IS_CHROMEOS)
// Allow paired devices which are not filtered above to appear in the UI.
if (device->IsPaired()) {
return false;
}
#endif
switch (InferDeviceTransport(device)) {
// For LE devices, check the discoverable flag and UUIDs.
case BLUETOOTH_TRANSPORT_LE:
// Hide the LE device that mark itself as non-discoverble.
if (device->GetAdvertisingDataFlags().has_value()) {
if (!((kLimitedDiscoveryFlag | kGeneralDiscoveryFlag) &
device->GetAdvertisingDataFlags().value())) {
return true;
}
}
// Check the service UUID to determine if it supports HID or second factor
// authenticator (security key).
if (base::Contains(device->GetUUIDs(),
device::BluetoothUUID(kHIDServiceUUID)) ||
base::Contains(device->GetUUIDs(),
device::BluetoothUUID(kSecurityKeyServiceUUID))) {
return false;
}
break;
// For classic mode devices, only filter out if the name is empty because
// the device could have an unknown or even known type and still also
// provide audio/HID functionality.
case BLUETOOTH_TRANSPORT_CLASSIC:
if (device->GetName()) {
return false;
}
break;
// Otherwise, they are invalid, so filter them out.
default:
break;
}
return true;
}
void RecordPairingResult(absl::optional<ConnectionFailureReason> failure_reason,
BluetoothTransport transport,
base::TimeDelta duration) {
RecordPairingTransport(transport);
std::string transport_name = GetTransportName(transport);
if (transport_name.empty()) {
return;
}
bool success = !failure_reason.has_value();
std::string result_histogram_name_prefix =
"Bluetooth.ChromeOS.Pairing.Result";
base::UmaHistogramBoolean(result_histogram_name_prefix, success);
base::UmaHistogramBoolean(result_histogram_name_prefix + "." + transport_name,
success);
std::string duration_histogram_name_prefix =
"Bluetooth.ChromeOS.Pairing.Duration";
std::string success_histogram_name = success ? "Success" : "Failure";
std::string base_histogram_name =
duration_histogram_name_prefix + "." + success_histogram_name;
RecordPairingDuration(base_histogram_name, duration);
RecordPairingDuration(base_histogram_name + "." + transport_name, duration);
if (!success) {
base::UmaHistogramEnumeration(
result_histogram_name_prefix + ".FailureReason", *failure_reason);
base::UmaHistogramEnumeration(
result_histogram_name_prefix + ".FailureReason." + transport_name,
*failure_reason);
EmitFilteredFailureReason(*failure_reason, transport_name);
}
}
void RecordUserInitiatedReconnectionAttemptResult(
absl::optional<ConnectionFailureReason> failure_reason,
UserInitiatedReconnectionUISurfaces surface) {
bool success = !failure_reason.has_value();
std::string base_histogram_name =
"Bluetooth.ChromeOS.UserInitiatedReconnectionAttempt.Result";
base::UmaHistogramBoolean(base_histogram_name, success);
std::string surface_name =
(surface == UserInitiatedReconnectionUISurfaces::kSettings
? "Settings"
: "SystemTray");
base::UmaHistogramBoolean(base_histogram_name + "." + surface_name, success);
if (!success) {
base::UmaHistogramEnumeration(base_histogram_name + ".FailureReason",
*failure_reason);
base::UmaHistogramEnumeration(
base_histogram_name + ".FailureReason." + surface_name,
*failure_reason);
}
}
void RecordDeviceSelectionDuration(base::TimeDelta duration,
DeviceSelectionUISurfaces surface,
bool was_paired,
BluetoothTransport transport) {
// Throw out longtail results of the user taking longer than
// |kMaxDeviceSelectionDuration|. Assume that these thrown out results reflect
// the user not being actively engaged with device connection: leaving the
// page open for a long time, walking away from computer, etc.
if (duration > kMaxDeviceSelectionDuration)
return;
std::string base_histogram_name =
"Bluetooth.ChromeOS.DeviceSelectionDuration";
RecordDeviceSelectionDuration(base_histogram_name, duration);
std::string surface_name =
(surface == DeviceSelectionUISurfaces::kSettings ? "Settings"
: "SystemTray");
std::string surface_histogram_name = base_histogram_name + "." + surface_name;
RecordDeviceSelectionDuration(surface_histogram_name, duration);
std::string paired_name = (was_paired ? "Paired" : "NotPaired");
std::string paired_histogram_name =
surface_histogram_name + "." + paired_name;
RecordDeviceSelectionDuration(paired_histogram_name, duration);
if (!was_paired) {
std::string transport_name = GetTransportName(transport);
if (transport_name.empty()) {
return;
}
std::string transport_histogram_name =
paired_histogram_name + "." + transport_name;
RecordDeviceSelectionDuration(transport_histogram_name, duration);
}
}
void RecordPoweredStateOperationResult(PoweredStateOperation operation,
bool success) {
std::string operation_name =
operation == PoweredStateOperation::kEnable ? "Enable" : "Disable";
base::UmaHistogramBoolean(base::StrCat({"Bluetooth.ChromeOS.PoweredState.",
operation_name, ".Result"}),
success);
}
void RecordPoweredState(bool is_powered) {
base::UmaHistogramBoolean("Bluetooth.ChromeOS.PoweredState", is_powered);
}
void RecordForgetResult(ForgetResult forget_result) {
base::UmaHistogramEnumeration("Bluetooth.ChromeOS.Forget.Result",
forget_result);
}
void RecordDeviceDisconnect(BluetoothDeviceType device_type) {
base::UmaHistogramEnumeration("Bluetooth.ChromeOS.DeviceDisconnect",
device_type);
}
void RecordUserInitiatedDisconnectResult(DisconnectResult disconnect_result,
BluetoothTransport transport) {
std::string transport_name = GetTransportName(transport);
if (transport_name.empty()) {
return;
}
base::UmaHistogramEnumeration(
"Bluetooth.ChromeOS.UserInitiatedDisconnect.Result", disconnect_result);
base::UmaHistogramEnumeration(
base::StrCat({"Bluetooth.ChromeOS.UserInitiatedDisconnect.Result.",
transport_name}),
disconnect_result);
}
void RecordUiSurfaceDisplayed(BluetoothUiSurface ui_surface) {
base::UmaHistogramEnumeration("Bluetooth.ChromeOS.UiSurfaceDisplayed",
ui_surface);
}
void RecordUserInitiatedReconnectionAttemptDuration(
absl::optional<ConnectionFailureReason> failure_reason,
BluetoothTransport transport,
base::TimeDelta duration) {
bool success = !failure_reason.has_value();
std::string transport_name = GetTransportName(transport);
if (transport_name.empty()) {
return;
}
std::string success_histogram_name = success ? "Success" : "Failure";
std::string base_histogram_name = base::StrCat(
{"Bluetooth.ChromeOS.UserInitiatedReconnectionAttempt.Duration.",
success_histogram_name});
base::UmaHistogramTimes(base_histogram_name, duration);
base::UmaHistogramTimes(
base::StrCat({base_histogram_name, ".", transport_name}), duration);
}
void RecordSetDeviceNickName(SetNicknameResult set_nickname_result) {
base::UmaHistogramEnumeration("Bluetooth.ChromeOS.SetNickname.Result",
set_nickname_result);
}
} // namespace device