| // Copyright 2018 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 "chromeos/components/tether/host_scanner_impl.h" |
| |
| #include <algorithm> |
| |
| #include "base/bind.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "chromeos/components/multidevice/logging/logging.h" |
| #include "chromeos/components/tether/connection_preserver.h" |
| #include "chromeos/components/tether/device_id_tether_network_guid_map.h" |
| #include "chromeos/components/tether/device_status_util.h" |
| #include "chromeos/components/tether/gms_core_notifications_state_tracker_impl.h" |
| #include "chromeos/components/tether/host_scan_cache.h" |
| #include "chromeos/components/tether/master_host_scan_cache.h" |
| #include "chromeos/components/tether/tether_host_fetcher.h" |
| #include "chromeos/network/network_state.h" |
| #include "chromeos/services/device_sync/remote_device_loader.h" |
| #include "components/session_manager/core/session_manager.h" |
| |
| namespace chromeos { |
| |
| namespace tether { |
| |
| HostScannerImpl::HostScannerImpl( |
| device_sync::DeviceSyncClient* device_sync_client, |
| secure_channel::SecureChannelClient* secure_channel_client, |
| NetworkStateHandler* network_state_handler, |
| session_manager::SessionManager* session_manager, |
| TetherHostFetcher* tether_host_fetcher, |
| HostScanDevicePrioritizer* host_scan_device_prioritizer, |
| TetherHostResponseRecorder* tether_host_response_recorder, |
| GmsCoreNotificationsStateTrackerImpl* gms_core_notifications_state_tracker, |
| NotificationPresenter* notification_presenter, |
| DeviceIdTetherNetworkGuidMap* device_id_tether_network_guid_map, |
| HostScanCache* host_scan_cache, |
| ConnectionPreserver* connection_preserver, |
| base::Clock* clock) |
| : device_sync_client_(device_sync_client), |
| secure_channel_client_(secure_channel_client), |
| network_state_handler_(network_state_handler), |
| session_manager_(session_manager), |
| tether_host_fetcher_(tether_host_fetcher), |
| host_scan_device_prioritizer_(host_scan_device_prioritizer), |
| tether_host_response_recorder_(tether_host_response_recorder), |
| gms_core_notifications_state_tracker_( |
| gms_core_notifications_state_tracker), |
| notification_presenter_(notification_presenter), |
| device_id_tether_network_guid_map_(device_id_tether_network_guid_map), |
| host_scan_cache_(host_scan_cache), |
| connection_preserver_(connection_preserver), |
| clock_(clock), |
| weak_ptr_factory_(this) { |
| session_manager_->AddObserver(this); |
| } |
| |
| HostScannerImpl::~HostScannerImpl() { |
| session_manager_->RemoveObserver(this); |
| } |
| |
| bool HostScannerImpl::IsScanActive() { |
| return is_fetching_hosts_ || host_scanner_operation_; |
| } |
| |
| void HostScannerImpl::StartScan() { |
| if (IsScanActive()) |
| return; |
| |
| is_fetching_hosts_ = true; |
| tether_host_fetcher_->FetchAllTetherHosts(base::Bind( |
| &HostScannerImpl::OnTetherHostsFetched, weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void HostScannerImpl::StopScan() { |
| if (!host_scanner_operation_) |
| return; |
| |
| PA_LOG(VERBOSE) << "Host scan has been stopped prematurely."; |
| |
| host_scanner_operation_->RemoveObserver( |
| gms_core_notifications_state_tracker_); |
| host_scanner_operation_->RemoveObserver(this); |
| host_scanner_operation_.reset(); |
| |
| NotifyScanFinished(); |
| } |
| |
| void HostScannerImpl::OnTetherHostsFetched( |
| const multidevice::RemoteDeviceRefList& tether_hosts) { |
| is_fetching_hosts_ = false; |
| |
| if (tether_hosts.empty()) { |
| PA_LOG(WARNING) << "Could not start host scan. No tether hosts available."; |
| return; |
| } |
| |
| PA_LOG(VERBOSE) << "Starting Tether host scan. " << tether_hosts.size() << " " |
| << "potential host(s) included in the search."; |
| |
| tether_guids_in_cache_before_scan_ = |
| host_scan_cache_->GetTetherGuidsInCache(); |
| |
| host_scanner_operation_ = HostScannerOperation::Factory::NewInstance( |
| tether_hosts, device_sync_client_, secure_channel_client_, |
| host_scan_device_prioritizer_, tether_host_response_recorder_, |
| connection_preserver_); |
| // Add |gms_core_notifications_state_tracker_| as the first observer. When the |
| // final change event is emitted, this class will destroy |
| // |host_scanner_operation_|, so |gms_core_notifications_state_tracker_| must |
| // be notified of the final change event before that occurs. |
| host_scanner_operation_->AddObserver(gms_core_notifications_state_tracker_); |
| host_scanner_operation_->AddObserver(this); |
| host_scanner_operation_->Initialize(); |
| } |
| |
| void HostScannerImpl::OnTetherAvailabilityResponse( |
| const std::vector<HostScannerOperation::ScannedDeviceInfo>& |
| scanned_device_list_so_far, |
| const multidevice::RemoteDeviceRefList& |
| gms_core_notifications_disabled_devices, |
| bool is_final_scan_result) { |
| if (scanned_device_list_so_far.empty() && !is_final_scan_result) { |
| was_notification_showing_when_current_scan_started_ = |
| IsPotentialHotspotNotificationShowing(); |
| } |
| |
| // Ensure all results received so far are in the cache (setting entries which |
| // already exist is a no-op). |
| for (const auto& scanned_device_info : scanned_device_list_so_far) |
| SetCacheEntry(scanned_device_info); |
| |
| if (CanAvailableHostNotificationBeShown() && |
| !scanned_device_list_so_far.empty()) { |
| if (scanned_device_list_so_far.size() == 1u && |
| (notification_presenter_->GetPotentialHotspotNotificationState() != |
| NotificationPresenter::PotentialHotspotNotificationState:: |
| MULTIPLE_HOTSPOTS_NEARBY_SHOWN || |
| is_final_scan_result)) { |
| multidevice::RemoteDeviceRef remote_device = |
| scanned_device_list_so_far.at(0).remote_device; |
| int32_t signal_strength; |
| NormalizeDeviceStatus(scanned_device_list_so_far.at(0).device_status, |
| nullptr /* carrier */, |
| nullptr /* battery_percentage */, &signal_strength); |
| notification_presenter_->NotifyPotentialHotspotNearby(remote_device, |
| signal_strength); |
| } else { |
| // Note: If a single-device notification was previously displayed, calling |
| // NotifyMultiplePotentialHotspotsNearby() will reuse the existing |
| // notification. |
| notification_presenter_->NotifyMultiplePotentialHotspotsNearby(); |
| } |
| |
| was_notification_shown_in_current_scan_ = true; |
| } |
| |
| if (is_final_scan_result) |
| OnFinalScanResultReceived(scanned_device_list_so_far); |
| } |
| |
| void HostScannerImpl::OnSessionStateChanged() { |
| if (!has_notification_been_shown_in_previous_scan_ || |
| !session_manager_->IsScreenLocked()) { |
| return; |
| } |
| |
| // If the screen has been locked, reset |
| // |has_notification_been_shown_in_previous_scan_|. This allows the |
| // notification to be shown once each time the device is unlocked. Without |
| // this change, the notification would only be shown once per user login. |
| // See https://crbug.com/813838. |
| PA_LOG(VERBOSE) |
| << "Screen was locked; the \"available hosts\" notification can " |
| << "be shown again after the next unlock."; |
| has_notification_been_shown_in_previous_scan_ = false; |
| } |
| |
| void HostScannerImpl::SetCacheEntry( |
| const HostScannerOperation::ScannedDeviceInfo& scanned_device_info) { |
| const DeviceStatus& status = scanned_device_info.device_status; |
| multidevice::RemoteDeviceRef remote_device = |
| scanned_device_info.remote_device; |
| |
| std::string carrier; |
| int32_t battery_percentage; |
| int32_t signal_strength; |
| NormalizeDeviceStatus(status, &carrier, &battery_percentage, |
| &signal_strength); |
| |
| host_scan_cache_->SetHostScanResult( |
| *HostScanCacheEntry::Builder() |
| .SetTetherNetworkGuid(device_id_tether_network_guid_map_ |
| ->GetTetherNetworkGuidForDeviceId( |
| remote_device.GetDeviceId())) |
| .SetDeviceName(remote_device.name()) |
| .SetCarrier(carrier) |
| .SetBatteryPercentage(battery_percentage) |
| .SetSignalStrength(signal_strength) |
| .SetSetupRequired(scanned_device_info.setup_required) |
| .Build()); |
| } |
| |
| void HostScannerImpl::OnFinalScanResultReceived( |
| const std::vector<HostScannerOperation::ScannedDeviceInfo>& |
| final_scan_results) { |
| // Search through all GUIDs that were in the cache before the scan began. If |
| // any of those GUIDs are not present in the final scan results, remove them |
| // from the cache. |
| for (const auto& tether_guid_in_cache : tether_guids_in_cache_before_scan_) { |
| bool is_guid_in_final_scan_results = false; |
| |
| for (const auto& scan_result : final_scan_results) { |
| if (device_id_tether_network_guid_map_->GetTetherNetworkGuidForDeviceId( |
| scan_result.remote_device.GetDeviceId()) == |
| tether_guid_in_cache) { |
| is_guid_in_final_scan_results = true; |
| break; |
| } |
| } |
| |
| if (!is_guid_in_final_scan_results) |
| host_scan_cache_->RemoveHostScanResult(tether_guid_in_cache); |
| } |
| |
| if (final_scan_results.empty()) { |
| RecordHostScanResult(HostScanResultEventType::NO_HOSTS_FOUND); |
| } else if (!was_notification_shown_in_current_scan_) { |
| RecordHostScanResult( |
| HostScanResultEventType::HOSTS_FOUND_BUT_NO_NOTIFICATION_SHOWN); |
| } else if (final_scan_results.size() == 1u) { |
| RecordHostScanResult( |
| HostScanResultEventType::NOTIFICATION_SHOWN_SINGLE_HOST); |
| } else { |
| RecordHostScanResult( |
| HostScanResultEventType::NOTIFICATION_SHOWN_MULTIPLE_HOSTS); |
| } |
| has_notification_been_shown_in_previous_scan_ |= |
| was_notification_shown_in_current_scan_; |
| was_notification_shown_in_current_scan_ = false; |
| was_notification_showing_when_current_scan_started_ = false; |
| |
| PA_LOG(VERBOSE) << "Finished Tether host scan. " << final_scan_results.size() |
| << " potential host(s) were found."; |
| |
| // If the final scan result has been received, the operation is finished. |
| // Delete it. |
| host_scanner_operation_->RemoveObserver( |
| gms_core_notifications_state_tracker_); |
| host_scanner_operation_->RemoveObserver(this); |
| host_scanner_operation_.reset(); |
| |
| NotifyScanFinished(); |
| } |
| |
| void HostScannerImpl::RecordHostScanResult(HostScanResultEventType event_type) { |
| DCHECK(event_type != HostScanResultEventType::HOST_SCAN_RESULT_MAX); |
| UMA_HISTOGRAM_ENUMERATION("InstantTethering.HostScanResult", event_type, |
| HostScanResultEventType::HOST_SCAN_RESULT_MAX); |
| } |
| |
| bool HostScannerImpl::IsPotentialHotspotNotificationShowing() { |
| return notification_presenter_->GetPotentialHotspotNotificationState() != |
| NotificationPresenter::PotentialHotspotNotificationState:: |
| NO_HOTSPOT_NOTIFICATION_SHOWN; |
| } |
| |
| bool HostScannerImpl::CanAvailableHostNotificationBeShown() { |
| const chromeos::NetworkTypePattern network_type_pattern = |
| chromeos::switches::ShouldTetherHostScansIgnoreWiredConnections() |
| ? chromeos::NetworkTypePattern::Wireless() |
| : chromeos::NetworkTypePattern::Default(); |
| // Note: If a network is active (i.e., connecting or connected), it will be |
| // returned at the front of the list, so using FirstNetworkByType() guarantees |
| // that we will find an active network if there is one. |
| const chromeos::NetworkState* first_network = |
| network_state_handler_->FirstNetworkByType(network_type_pattern); |
| if (first_network && first_network->IsConnectingOrConnected()) { |
| // If a network is connecting or connected, the notification should not be |
| // shown. |
| return false; |
| } |
| |
| if (!IsPotentialHotspotNotificationShowing() && |
| was_notification_shown_in_current_scan_) { |
| // If a notification was shown in the current scan but it is no longer |
| // showing, it has been removed, either due to NotificationRemover or due to |
| // the user closing it. Since a scan only lasts on the order of seconds to |
| // tens of seconds, we know that the notification was very recently closed, |
| // so we should not re-show it. |
| return false; |
| } |
| |
| if (!IsPotentialHotspotNotificationShowing() && |
| was_notification_showing_when_current_scan_started_) { |
| // If a notification was showing when the scan started but is no longer |
| // showing, it has been removed and should not be re-shown. |
| return false; |
| } |
| |
| if (has_notification_been_shown_in_previous_scan_ && |
| !was_notification_showing_when_current_scan_started_) { |
| // If a notification was shown in a previous scan but was not visible when |
| // the current scan started, it should not be shown because this could be |
| // considered spammy; see https://crbug.com/759078. |
| return false; |
| } |
| |
| return true; |
| } |
| |
| } // namespace tether |
| |
| } // namespace chromeos |