chromium/ash/system/phonehub/phone_hub_ui_controller.cc

// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ash/system/phonehub/phone_hub_ui_controller.h"

#include <memory>

#include "ash/constants/ash_features.h"
#include "ash/root_window_controller.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/system/eche/eche_tray.h"
#include "ash/system/phonehub/app_stream_launcher_view.h"
#include "ash/system/phonehub/bluetooth_disabled_view.h"
#include "ash/system/phonehub/onboarding_view.h"
#include "ash/system/phonehub/phone_connected_view.h"
#include "ash/system/phonehub/phone_connecting_view.h"
#include "ash/system/phonehub/phone_disconnected_view.h"
#include "ash/system/phonehub/phone_hub_content_view.h"
#include "ash/system/phonehub/tether_connection_pending_view.h"
#include "ash/system/status_area_widget.h"
#include "base/functional/bind.h"
#include "base/metrics/histogram_functions.h"
#include "base/time/time.h"
#include "chromeos/ash/components/multidevice/logging/logging.h"
#include "chromeos/ash/components/phonehub/browser_tabs_model_provider.h"
#include "chromeos/ash/components/phonehub/connection_scheduler.h"
#include "chromeos/ash/components/phonehub/phone_hub_manager.h"
#include "chromeos/ash/components/phonehub/phone_hub_structured_metrics_logger.h"
#include "chromeos/ash/components/phonehub/phone_hub_ui_readiness_recorder.h"
#include "chromeos/ash/components/phonehub/tether_controller.h"
#include "chromeos/ash/components/phonehub/user_action_recorder.h"

namespace ash {

namespace {

using FeatureStatus = phonehub::FeatureStatus;
using TetherStatus = phonehub::TetherController::Status;

constexpr base::TimeDelta kConnectingViewGracePeriod = base::Seconds(40);

phone_hub_metrics::Screen GetMetricsScreen(
    PhoneHubUiController::UiState ui_state) {
  switch (ui_state) {
    case PhoneHubUiController::UiState::kOnboardingWithoutPhone:
      return phone_hub_metrics::Screen::kOnboardingNewMultideviceUser;

    case PhoneHubUiController::UiState::kOnboardingWithPhone:
      return phone_hub_metrics::Screen::kOnboardingExistingMultideviceUser;

    case PhoneHubUiController::UiState::kPhoneConnected:
      return phone_hub_metrics::Screen::kPhoneConnected;

    case PhoneHubUiController::UiState::kPhoneDisconnected:
      return phone_hub_metrics::Screen::kPhoneDisconnected;

    case PhoneHubUiController::UiState::kPhoneConnecting:
      return phone_hub_metrics::Screen::kPhoneConnecting;

    case PhoneHubUiController::UiState::kBluetoothDisabled:
      return phone_hub_metrics::Screen::kBluetoothOrWifiDisabled;

    case PhoneHubUiController::UiState::kTetherConnectionPending:
      return phone_hub_metrics::Screen::kTetherConnectionPending;

    case PhoneHubUiController::UiState::kMiniLauncher:
      return phone_hub_metrics::Screen::kMiniLauncher;

    case PhoneHubUiController::UiState::kHidden:
      return phone_hub_metrics::Screen::kInvalid;
  }
}

std::string PhoneHubUIStateToString(PhoneHubUiController::UiState ui_state) {
  switch (ui_state) {
    case PhoneHubUiController::UiState::kOnboardingWithoutPhone:
      return "[kOnboardingWithoutPhone]";

    case PhoneHubUiController::UiState::kOnboardingWithPhone:
      return "[kOnboardingWithPhone]";

    case PhoneHubUiController::UiState::kPhoneConnected:
      return "[kPhoneConnected]";

    case PhoneHubUiController::UiState::kPhoneDisconnected:
      return "[kPhoneDisconnected]";

    case PhoneHubUiController::UiState::kPhoneConnecting:
      return "[kPhoneConnecting]";

    case PhoneHubUiController::UiState::kBluetoothDisabled:
      return "[kBluetoothDisabled]";

    case PhoneHubUiController::UiState::kTetherConnectionPending:
      return "[kTetherConnectionPending]";

    case PhoneHubUiController::UiState::kMiniLauncher:
      return "[kMiniLauncher]";

    case PhoneHubUiController::UiState::kHidden:
      return "[kHidden]";
  }
}

std::string FeatureStatusToString(FeatureStatus feature_status) {
  switch (feature_status) {
    case FeatureStatus::kNotEligibleForFeature:
      return "[kNotEligibleForFeature]";
    case FeatureStatus::kEligiblePhoneButNotSetUp:
      return "[kEligiblePhoneButNotSetUp]";
    case FeatureStatus::kPhoneSelectedAndPendingSetup:
      return "[kPhoneSelectedAndPendingSetup]";
    case FeatureStatus::kDisabled:
      return "[kDisabled]";
    case FeatureStatus::kUnavailableBluetoothOff:
      return "[kUnavailableBluetoothOff]";
    case FeatureStatus::kEnabledButDisconnected:
      return "[kEnabledButDisconnected]";
    case FeatureStatus::kEnabledAndConnecting:
      return "[kEnabledAndConnecting]";
    case FeatureStatus::kEnabledAndConnected:
      return "[kEnabledAndConnected]";
    case FeatureStatus::kLockOrSuspended:
      return "[kLockOrSuspended]";
  }
}

}  // namespace

PhoneHubUiController::PhoneHubUiController() {
  // ash::Shell may not exist in tests.
  if (ash::Shell::HasInstance())
    Shell::Get()->session_controller()->AddObserver(this);
}

PhoneHubUiController::~PhoneHubUiController() {
  // ash::Shell may not exist in tests.
  if (ash::Shell::HasInstance())
    Shell::Get()->session_controller()->RemoveObserver(this);
  CleanUpPhoneHubManager();
}

void PhoneHubUiController::SetPhoneHubManager(
    phonehub::PhoneHubManager* phone_hub_manager) {
  if (phone_hub_manager == phone_hub_manager_)
    return;

  CleanUpPhoneHubManager();

  phone_hub_manager_ = phone_hub_manager;
  if (phone_hub_manager_) {
    phone_hub_manager_->GetFeatureStatusProvider()->AddObserver(this);
    phone_hub_manager_->GetOnboardingUiTracker()->AddObserver(this);
    if (features::IsEcheLauncherEnabled())
      phone_hub_manager_->GetAppStreamLauncherDataModel()->AddObserver(this);
    phone_hub_manager_->GetPhoneModel()->AddObserver(this);
  }

  UpdateUiState(GetUiStateFromPhoneHubManager());
}

std::unique_ptr<views::View> PhoneHubUiController::CreateStatusHeaderView(
    PhoneStatusView::Delegate* delegate) {
  if (!phone_hub_manager_)
    return nullptr;
  return std::make_unique<PhoneStatusView>(phone_hub_manager_->GetPhoneModel(),
                                           delegate);
}

std::unique_ptr<PhoneHubContentView> PhoneHubUiController::CreateContentView(
    OnboardingView::Delegate* delegate) {
  PA_LOG(VERBOSE) << __func__
                  << ": ui state = " << PhoneHubUIStateToString(ui_state_);

  switch (ui_state_) {
    case UiState::kHidden:
      return nullptr;
    case UiState::kOnboardingWithoutPhone:
      return std::make_unique<OnboardingView>(
          phone_hub_manager_->GetOnboardingUiTracker(), delegate,
          OnboardingView::kNewMultideviceUser);
    case UiState::kOnboardingWithPhone:
      return std::make_unique<OnboardingView>(
          phone_hub_manager_->GetOnboardingUiTracker(), delegate,
          OnboardingView::kExistingMultideviceUser);
    case UiState::kBluetoothDisabled:
      return std::make_unique<BluetoothDisabledView>();
    case UiState::kPhoneConnecting:
      return std::make_unique<PhoneConnectingView>();
    case UiState::kTetherConnectionPending:
      return std::make_unique<TetherConnectionPendingView>();
    case UiState::kPhoneDisconnected:
      if (connecting_view_grace_period_timer_.IsRunning())
        return std::make_unique<PhoneConnectingView>();
      return std::make_unique<PhoneDisconnectedView>(
          phone_hub_manager_->GetConnectionScheduler());
    case UiState::kPhoneConnected:
      return std::make_unique<PhoneConnectedView>(phone_hub_manager_);
    case UiState::kMiniLauncher:
      return std::make_unique<AppStreamLauncherView>(phone_hub_manager_);
  }
}

void PhoneHubUiController::HandleBubbleOpened() {
  // Make sure Eche window is not shown.
  if (features::IsEcheSWAEnabled()) {
    EcheTray* eche_tray = Shell::GetPrimaryRootWindowController()
                              ->GetStatusAreaWidget()
                              ->eche_tray();
    if (eche_tray)
      eche_tray->CloseBubble();
  }

  if (!phone_hub_manager_)
    return;

  auto feature_status =
      phone_hub_manager_->GetFeatureStatusProvider()->GetStatus();
  if (feature_status == FeatureStatus::kEnabledButDisconnected)
    phone_hub_manager_->GetConnectionScheduler()->ScheduleConnectionNow(
        phonehub::DiscoveryEntryPoint::kPhoneHubBubbleOpen);

  if (features::IsEcheNetworkConnectionStateEnabled() &&
      feature_status == FeatureStatus::kEnabledAndConnected) {
    if (phone_hub_manager_->GetEcheConnectionStatusHandler()) {
      phone_hub_manager_->GetEcheConnectionStatusHandler()
          ->CheckConnectionStatusForUi();
    }
  }

  phone_hub_manager_->GetBrowserTabsModelProvider()->TriggerRefresh();
  RecordStatusOnBubbleOpened();

  bool is_feature_enabled =
      feature_status == FeatureStatus::kEnabledAndConnected ||
      feature_status == FeatureStatus::kEnabledButDisconnected ||
      feature_status == FeatureStatus::kEnabledAndConnected;

  if (!is_feature_enabled) {
    PA_LOG(VERBOSE) << __func__ << ": feature is not enabled. Feature status = "
                    << FeatureStatusToString(feature_status);
    return;
  }

  PA_LOG(VERBOSE) << __func__ << ": feature is enabled. Feature status = "
                  << FeatureStatusToString(feature_status);

  if (!has_requested_tether_scan_during_session_ &&
      phone_hub_manager_->GetTetherController()->GetStatus() ==
          TetherStatus::kConnectionUnavailable) {
    phone_hub_manager_->GetTetherController()->ScanForAvailableConnection();
    has_requested_tether_scan_during_session_ = true;
  }
}

void PhoneHubUiController::RecordStatusOnBubbleOpened() {
  switch (ui_state_) {
    case UiState::kHidden:
    case UiState::kOnboardingWithoutPhone:
    case UiState::kOnboardingWithPhone:
    case UiState::kBluetoothDisabled:
    case UiState::kTetherConnectionPending:
      return;

    case UiState::kMiniLauncher:
    case UiState::kPhoneConnected:
      base::UmaHistogramEnumeration("PhoneHub.BubbleOpened.Connectable.Page",
                                    phone_hub_metrics::Screen::kPhoneConnected);
      return;

    case UiState::kPhoneDisconnected:
    case UiState::kPhoneConnecting:
      phone_hub_manager_->GetHostLastSeenTimestamp(
          base::BindOnce(&PhoneHubUiController::OnGetHostLastSeenTimestamp,
                         weak_ptr_factory_.GetWeakPtr(), ui_state_));
      return;
  }
}

void PhoneHubUiController::OnGetHostLastSeenTimestamp(
    UiState ui_state_when_opened,
    std::optional<base::Time> timestamp) {
  if (timestamp) {
    base::UmaHistogramLongTimes(
        "PhoneHub.BubbleOpened.Connectable.Failed.HostLastSeen",
        base::Time::Now() - timestamp.value());
  }

  // Only log when we've seen the host phone within the last 2 minutes.
  if (!timestamp || base::Time::Now() - timestamp.value() > base::Minutes(2)) {
    return;
  }

  base::UmaHistogramEnumeration("PhoneHub.BubbleOpened.Connectable.Page",
                                GetMetricsScreen(ui_state_when_opened));
}

void PhoneHubUiController::AddObserver(Observer* observer) {
  observer_list_.AddObserver(observer);
}

void PhoneHubUiController::RemoveObserver(Observer* observer) {
  observer_list_.RemoveObserver(observer);
}

void PhoneHubUiController::OnFeatureStatusChanged() {
  UpdateUiState(GetUiStateFromPhoneHubManager());
}

void PhoneHubUiController::OnShouldShowOnboardingUiChanged() {
  UpdateUiState(GetUiStateFromPhoneHubManager());
}

void PhoneHubUiController::OnShouldShowMiniLauncherChanged() {
  if (!features::IsEcheLauncherEnabled())
    return;
  UpdateUiState(GetUiStateFromPhoneHubManager());
}

void PhoneHubUiController::OnModelChanged() {
  PA_LOG(INFO) << "Updating UI status as Phone Model has changed";
  UpdateUiState(GetUiStateFromPhoneHubManager());
}

void PhoneHubUiController::OnActiveUserSessionChanged(
    const AccountId& account_id) {
  UpdateUiState(GetUiStateFromPhoneHubManager());
}

void PhoneHubUiController::UpdateUiState(
    PhoneHubUiController::UiState new_state) {
  if (new_state == ui_state_)
    return;

  PA_LOG(VERBOSE) << __func__
                  << ": old ui = " << PhoneHubUIStateToString(ui_state_)
                  << ", new ui = " << PhoneHubUIStateToString(new_state);
  ui_state_ = new_state;

  for (auto& observer : observer_list_) {
    observer.OnPhoneHubUiStateChanged();
  }

  switch (ui_state_) {
    case UiState::kBluetoothDisabled:
      [[fallthrough]];
    case UiState::kPhoneDisconnected:
      if (phone_hub_manager_->GetPhoneHubStructuredMetricsLogger()) {
        phone_hub_manager_->GetPhoneHubStructuredMetricsLogger()
            ->LogPhoneHubUiStateUpdated(
                phonehub::PhoneHubUiState::kDisconnected);
        phone_hub_manager_->GetPhoneHubStructuredMetricsLogger()
            ->ResetSessionId();
      }
      break;
    case UiState::kPhoneConnecting:
      if (phone_hub_manager_->GetPhoneHubStructuredMetricsLogger()) {
        phone_hub_manager_->GetPhoneHubStructuredMetricsLogger()
            ->LogPhoneHubUiStateUpdated(phonehub::PhoneHubUiState::kConnecting);
      }
      break;
    case UiState::kPhoneConnected:
      if (phone_hub_manager_->GetPhoneHubUiReadinessRecorder()) {
        phone_hub_manager_->GetPhoneHubUiReadinessRecorder()
            ->RecordPhoneHubUiConnected();
      }
      if (phone_hub_manager_->GetPhoneHubStructuredMetricsLogger()) {
        phone_hub_manager_->GetPhoneHubStructuredMetricsLogger()
            ->LogPhoneHubUiStateUpdated(phonehub::PhoneHubUiState::kConnected);
      }
      break;
    default:
      break;
  }
}

PhoneHubUiController::UiState
PhoneHubUiController::GetUiStateFromPhoneHubManager() {
  PhoneHubUiController::UiState ui_state =
      GetUiStateFromPhoneHubManagerInternal();
  if (features::IsEcheLauncherEnabled() &&
      (ui_state != PhoneHubUiController::UiState::kMiniLauncher) &&
      phone_hub_manager_ &&
      phone_hub_manager_->GetAppStreamLauncherDataModel()) {
    // Make sure the next time we go back to the "Phone Connected" state
    // we do not show the Mini Launcher.
    phone_hub_manager_->GetAppStreamLauncherDataModel()->ResetState();
  }
  return ui_state;
}

PhoneHubUiController::UiState
PhoneHubUiController::GetUiStateFromPhoneHubManagerInternal() {
  if (!Shell::Get()->session_controller()->IsUserPrimary() ||
      !phone_hub_manager_)
    return UiState::kHidden;

  auto feature_status =
      phone_hub_manager_->GetFeatureStatusProvider()->GetStatus();

  auto* tracker = phone_hub_manager_->GetOnboardingUiTracker();
  auto* phone_model = phone_hub_manager_->GetPhoneModel();
  bool should_show_onboarding_ui = tracker->ShouldShowOnboardingUi();
  bool is_tether_connecting =
      phone_hub_manager_->GetTetherController()->GetStatus() ==
      TetherStatus::kConnecting;

  switch (feature_status) {
    case FeatureStatus::kPhoneSelectedAndPendingSetup:
      [[fallthrough]];
    case FeatureStatus::kNotEligibleForFeature:
      return UiState::kHidden;

    case FeatureStatus::kEligiblePhoneButNotSetUp:
      return should_show_onboarding_ui ? UiState::kOnboardingWithoutPhone
                                       : UiState::kHidden;

    case FeatureStatus::kDisabled:
      return should_show_onboarding_ui ? UiState::kOnboardingWithPhone
                                       : UiState::kHidden;

    case FeatureStatus::kUnavailableBluetoothOff:
      return UiState::kBluetoothDisabled;

    case FeatureStatus::kEnabledButDisconnected:
      return UiState::kPhoneDisconnected;

    case FeatureStatus::kEnabledAndConnecting:
      connecting_view_grace_period_timer_.Start(
          FROM_HERE, kConnectingViewGracePeriod,
          base::BindOnce(&PhoneHubUiController::OnConnectingViewTimerEnd,
                         base::Unretained(this)));

      // If a tether network is being connected to, or the |ui_state_|
      // was UiState::kTetherConnectionPending, continue returning
      // the UiState::kTetherConnectionPending state.
      return is_tether_connecting ||
                     ui_state_ == UiState::kTetherConnectionPending
                 ? UiState::kTetherConnectionPending
                 : UiState::kPhoneConnecting;

    case FeatureStatus::kEnabledAndConnected:
      // If the timer is running, reset the timer so if we disconnect, we will
      // show the connecting view instead of the disconnecting view.
      if (connecting_view_grace_period_timer_.IsRunning())
        connecting_view_grace_period_timer_.Reset();

      // Delay displaying the connected view until the phone model is ready.
      if (phone_model->phone_status_model().has_value()) {
        // Decide to show the Mini Launcher or the main connected phone view.
        return phone_hub_manager_->GetAppStreamLauncherDataModel()
                           ->GetShouldShowMiniLauncher() &&
                       features::IsEcheSWAEnabled() &&
                       features::IsEcheLauncherEnabled()
                   ? UiState::kMiniLauncher
                   : UiState::kPhoneConnected;
      }

      // If the the |ui_state_| was UiState::kTetherConnectionPending, continue
      // returning the UiState::kTetherConnectionPending state.
      if (ui_state_ == UiState::kTetherConnectionPending)
        return UiState::kTetherConnectionPending;

      return UiState::kPhoneConnecting;

    case FeatureStatus::kLockOrSuspended:
      return UiState::kHidden;
  }
}

void PhoneHubUiController::OnConnectingViewTimerEnd() {
  // Update the UI state if the UI state has changed.
  if (ui_state_ != UiState::kPhoneDisconnected) {
    UpdateUiState(GetUiStateFromPhoneHubManager());
    return;
  }

  // If we are still disconnected, force the observation. We cannot call
  // |GetUiStateFromPhoneHubManager()| in this case because it will reset the
  // timer, and thus the disconnected view will never be shown. This way, the
  // disconnected view will be shown.
  for (auto& observer : observer_list_)
    observer.OnPhoneHubUiStateChanged();
}

void PhoneHubUiController::CleanUpPhoneHubManager() {
  if (!phone_hub_manager_)
    return;

  phone_hub_manager_->GetFeatureStatusProvider()->RemoveObserver(this);
  phone_hub_manager_->GetOnboardingUiTracker()->RemoveObserver(this);
  if (features::IsEcheSWAEnabled())
    phone_hub_manager_->GetAppStreamLauncherDataModel()->RemoveObserver(this);
  phone_hub_manager_->GetPhoneModel()->RemoveObserver(this);
}

}  // namespace ash