| // 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 |