chromium/chromeos/ash/components/tether/host_scanner_impl.cc

// 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 "chromeos/ash/components/tether/host_scanner_impl.h"

#include <algorithm>

#include "ash/constants/ash_switches.h"
#include "base/functional/bind.h"
#include "base/metrics/histogram_macros.h"
#include "base/trace_event/trace_event.h"
#include "chromeos/ash/components/multidevice/logging/logging.h"
#include "chromeos/ash/components/network/network_state.h"
#include "chromeos/ash/components/tether/connection_preserver.h"
#include "chromeos/ash/components/tether/device_id_tether_network_guid_map.h"
#include "chromeos/ash/components/tether/device_status_util.h"
#include "chromeos/ash/components/tether/gms_core_notifications_state_tracker_impl.h"
#include "chromeos/ash/components/tether/host_scan_cache.h"
#include "chromeos/ash/components/tether/tether_host_fetcher.h"
#include "chromeos/ash/components/tether/top_level_host_scan_cache.h"
#include "chromeos/ash/services/secure_channel/public/cpp/client/secure_channel_client.h"
#include "components/session_manager/core/session_manager.h"

namespace ash::tether {

HostScannerImpl::HostScannerImpl(
    std::unique_ptr<TetherAvailabilityOperationOrchestrator::Factory>
        tether_availability_operation_orchestrator_factory,
    NetworkStateHandler* network_state_handler,
    session_manager::SessionManager* session_manager,
    GmsCoreNotificationsStateTrackerImpl* gms_core_notifications_state_tracker,
    NotificationPresenter* notification_presenter,
    DeviceIdTetherNetworkGuidMap* device_id_tether_network_guid_map,
    HostScanCache* host_scan_cache,
    base::Clock* clock)
    : network_state_handler_(network_state_handler),
      session_manager_(session_manager),
      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),
      clock_(clock),
      tether_availability_operation_orchestrator_factory_(
          std::move(tether_availability_operation_orchestrator_factory)) {
  session_manager_->AddObserver(this);
}

HostScannerImpl::~HostScannerImpl() {
  session_manager_->RemoveObserver(this);
}

bool HostScannerImpl::IsScanActive() {
  return tether_availability_operation_orchestrator_ != nullptr;
}

void HostScannerImpl::StartScan() {
  if (IsScanActive()) {
    PA_LOG(ERROR)
        << "Not starting host scan, as a tether host scan is already active.";
    return;
  }

  PA_LOG(VERBOSE) << "Starting tether host scan.";

  tether_guids_in_cache_before_scan_ =
      host_scan_cache_->GetTetherGuidsInCache();

  PA_LOG(VERBOSE) << "Constructing TetherAvailabilityOperationOrchestrator";
  tether_availability_operation_orchestrator_ =
      tether_availability_operation_orchestrator_factory_->CreateInstance();

  tether_availability_operation_orchestrator_->AddObserver(
      gms_core_notifications_state_tracker_);
  tether_availability_operation_orchestrator_->AddObserver(this);

  PA_LOG(VERBOSE) << "Starting TetherAvailabilityOperationOrchestrator";
  tether_availability_operation_orchestrator_->Start();
}

void HostScannerImpl::StopScan() {
  if (!tether_availability_operation_orchestrator_) {
    return;
  }

  PA_LOG(VERBOSE) << "Host scan has been stopped prematurely.";

  tether_availability_operation_orchestrator_->RemoveObserver(
      gms_core_notifications_state_tracker_);
  tether_availability_operation_orchestrator_->RemoveObserver(this);
  tether_availability_operation_orchestrator_.reset();

  NotifyScanFinished();
}

void HostScannerImpl::OnTetherAvailabilityResponse(
    const std::vector<ScannedDeviceInfo>& scanned_device_list_so_far,
    const std::vector<ScannedDeviceInfo>&
        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();
    PA_LOG(VERBOSE) << "Was 'potential hotspot' notification showing when "
                       "current scan started: ["
                    << was_notification_showing_when_current_scan_started_
                    << "]";
  }

  // 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)) {
      const ScannedDeviceInfo& scanned_device =
          scanned_device_list_so_far.front();
      int32_t signal_strength;
      NormalizeDeviceStatus(scanned_device.device_status.value(),
                            nullptr /* carrier */,
                            nullptr /* battery_percentage */, &signal_strength);
      notification_presenter_->NotifyPotentialHotspotNearby(
          scanned_device.device_id, scanned_device.device_name,
          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() {
  TRACE_EVENT0("login", "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 ScannedDeviceInfo& scanned_device_info) {
  const DeviceStatus& status = scanned_device_info.device_status.value();

  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(
                                         scanned_device_info.device_id))
           .SetDeviceName(scanned_device_info.device_name)
           .SetCarrier(carrier)
           .SetBatteryPercentage(battery_percentage)
           .SetSignalStrength(signal_strength)
           .SetSetupRequired(scanned_device_info.setup_required)
           .Build());
}

void HostScannerImpl::OnFinalScanResultReceived(
    const std::vector<ScannedDeviceInfo>& final_scan_results) {
  PA_LOG(INFO) << __func__;
  // 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.device_id) == 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.
  tether_availability_operation_orchestrator_->RemoveObserver(
      gms_core_notifications_state_tracker_);
  tether_availability_operation_orchestrator_->RemoveObserver(this);
  tether_availability_operation_orchestrator_.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 NetworkTypePattern network_type_pattern =
      switches::ShouldTetherHostScansIgnoreWiredConnections()
          ? NetworkTypePattern::Wireless()
          : 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 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.
    PA_LOG(INFO)
        << __func__
        << " Returning false because device is connected/connecting to "
           "a network";
    return false;
  }

  if (!IsPotentialHotspotNotificationShowing() &&
      was_notification_shown_in_current_scan_) {
    PA_LOG(INFO) << __func__
                 << " Returning false because notification has been shown in "
                    "the 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_) {
    PA_LOG(INFO) << __func__
                 << " Returning false because notification was showing when "
                    "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;
  }

  PA_LOG(INFO) << __func__ << " Returning true because all checks passed";

  return true;
}

}  // namespace ash::tether