chromium/chrome/browser/ash/floating_workspace/floating_workspace_service.cc

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

#include "chrome/browser/ash/floating_workspace/floating_workspace_service.h"

#include <algorithm>
#include <cstddef>
#include <string>
#include <vector>

#include "ash/constants/ash_features.h"
#include "ash/public/cpp/desk_template.h"
#include "ash/public/cpp/notification_utils.h"
#include "ash/public/cpp/session/session_controller.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/system/tray/system_tray_notifier.h"
#include "ash/webui/settings/public/constants/routes.mojom-forward.h"
#include "ash/wm/desks/desk.h"
#include "ash/wm/desks/templates/saved_desk_metrics_util.h"
#include "ash/wm/desks/templates/saved_desk_util.h"
#include "base/check.h"
#include "base/memory/raw_ptr.h"
#include "base/task/single_thread_task_runner.h"
#include "base/time/time.h"
#include "base/uuid.h"
#include "chrome/app/vector_icons/vector_icons.h"
#include "chrome/browser/ash/floating_workspace/floating_workspace_metrics_util.h"
#include "chrome/browser/ash/floating_workspace/floating_workspace_service_factory.h"
#include "chrome/browser/ash/floating_workspace/floating_workspace_util.h"
#include "chrome/browser/ash/login/session/user_session_manager.h"
#include "chrome/browser/ash/profiles/profile_helper.h"
#include "chrome/browser/lifetime/application_lifetime.h"
#include "chrome/browser/notifications/notification_display_service.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/sessions/session_restore.h"
#include "chrome/browser/sync/desk_sync_service_factory.h"
#include "chrome/browser/sync/device_info_sync_service_factory.h"
#include "chrome/browser/sync/session_sync_service_factory.h"
#include "chrome/browser/sync/sync_service_factory.h"
#include "chrome/browser/ui/ash/desks/desks_client.h"
#include "chrome/browser/ui/ash/multi_user/multi_user_util.h"
#include "chrome/browser/ui/settings_window_manager_chromeos.h"
#include "chrome/grit/generated_resources.h"
#include "chromeos/ash/components/network/network_handler.h"
#include "chromeos/ash/components/network/network_state_handler.h"
#include "components/app_constants/constants.h"
#include "components/desks_storage/core/desk_model.h"
#include "components/desks_storage/core/desk_sync_bridge.h"
#include "components/desks_storage/core/desk_sync_service.h"
#include "components/sync/base/data_type.h"
#include "components/sync/service/sync_service.h"
#include "components/sync_device_info/device_info.h"
#include "components/sync_device_info/device_info_sync_service.h"
#include "components/sync_device_info/local_device_info_provider_impl.h"
#include "components/sync_sessions/open_tabs_ui_delegate.h"
#include "components/sync_sessions/session_sync_service.h"
#include "components/sync_sessions/synced_session.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/user_activity/user_activity_detector.h"
#include "ui/chromeos/devicetype_utils.h"
#include "ui/message_center/public/cpp/notification.h"

namespace ash {

constexpr char kNotificationForNoNetworkConnection[] =
    "notification_no_network_connection";
constexpr char kNotificationForSyncErrorOrTimeOut[] =
    "notification_sync_error_or_timeout";
constexpr char kNotificationForRestoreAfterError[] =
    "notification_restore_after_error";
constexpr char kNotificationForProgressStatus[] =
    "notification_progress_status";
constexpr char kSafeMode[] = "notification_safe_mode";
// Default time without activity after which a floating workspace template is
// considered stale and becomes a candidate for garbage collection.
constexpr base::TimeDelta kStaleFWSThreshold = base::Days(30);
// Minimum time to wait before we decide to show the progress status if no
// floating workspace templates have been downloaded yet.
constexpr base::TimeDelta kMinTimeToWait = base::Seconds(2);
// Time interval between progress bar update.
constexpr base::TimeDelta kProgressTimeUpdateDelay = base::Seconds(1);

FloatingWorkspaceServiceNotificationType GetNotificationTypeById(
    const std::string& id) {
  if (id == kNotificationForNoNetworkConnection) {
    return FloatingWorkspaceServiceNotificationType::kNoNetworkConnection;
  }
  if (id == kNotificationForSyncErrorOrTimeOut) {
    return FloatingWorkspaceServiceNotificationType::kSyncErrorOrTimeOut;
  }
  if (id == kNotificationForRestoreAfterError) {
    return FloatingWorkspaceServiceNotificationType::kRestoreAfterError;
  }
  if (id == kNotificationForProgressStatus) {
    return FloatingWorkspaceServiceNotificationType::kProgressStatus;
  }
  if (id == kSafeMode) {
    return FloatingWorkspaceServiceNotificationType::kSafeMode;
  }
  return FloatingWorkspaceServiceNotificationType::kUnknown;
}

// Static
FloatingWorkspaceService* FloatingWorkspaceService::GetForProfile(
    Profile* profile) {
  return static_cast<FloatingWorkspaceService*>(
      FloatingWorkspaceServiceFactory::GetInstance()->GetForProfile(profile));
}

FloatingWorkspaceService::FloatingWorkspaceService(
    Profile* profile,
    floating_workspace_util::FloatingWorkspaceVersion version)
    : profile_(profile),
      version_(version),
      initialization_timeticks_(base::TimeTicks::Now()),
      initialization_time_(base::Time::Now()) {}

FloatingWorkspaceService::~FloatingWorkspaceService() {
  StopCaptureAndUploadActiveDesk();
  ShutDownServicesAndObservers();
  if (ash::SessionController::Get()) {
    ash::SessionController::Get()->RemoveObserver(this);
  }
}

void FloatingWorkspaceService::OnSyncShutdown(syncer::SyncService* sync) {
  if (sync_service_ && sync_service_->HasObserver(this)) {
    sync_service_->RemoveObserver(this);
  }
  sync_service_ = nullptr;
}

void FloatingWorkspaceService::OnShuttingDown() {
  if (ash::NetworkHandler::IsInitialized()) {
    auto* network_handler = NetworkHandler::Get();
    if (network_handler->network_state_handler()->HasObserver(this)) {
      network_handler->network_state_handler()->RemoveObserver(this);
    }
  }
}

void FloatingWorkspaceService::OnDeviceInfoShutdown() {
  if (device_info_sync_service_ &&
      device_info_sync_service_->GetDeviceInfoTracker()) {
    device_info_sync_service_->GetDeviceInfoTracker()->RemoveObserver(this);
  }
  device_info_sync_service_ = nullptr;
}

void FloatingWorkspaceService::InitiateSigninTask() {
  if (!floating_workspace_util::IsInternetConnected()) {
    SendNotification(kNotificationForNoNetworkConnection);
  } else {
    syncer::LocalDeviceInfoProviderImpl* local_device_info_provider =
        static_cast<syncer::LocalDeviceInfoProviderImpl*>(
            device_info_sync_service_->GetLocalDeviceInfoProvider());

    local_device_info_ready_subscription_ =
        local_device_info_provider->RegisterOnInitializedCallback(
            base::BindRepeating(
                &FloatingWorkspaceService::OnLocalDeviceInfoProviderReady,
                weak_pointer_factory_.GetWeakPtr()));
  }

    base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
        FROM_HERE,
        base::BindOnce(
            &FloatingWorkspaceService::MaybeStartProgressBarNotification,
            weak_pointer_factory_.GetWeakPtr()),
        kMinTimeToWait);
}

// TODO(b/309137462): Clean up params to not need to be passed in.
void FloatingWorkspaceService::Init(
    syncer::SyncService* sync_service,
    desks_storage::DeskSyncService* desk_sync_service,
    syncer::DeviceInfoSyncService* device_info_sync_service) {
  if (ash::SessionController::Get()) {
    ash::SessionController::Get()->AddObserver(this);
  }

  if (version_ == floating_workspace_util::FloatingWorkspaceVersion::
                      kFloatingWorkspaceV1Enabled) {
    floating_workspace_metrics_util::
        RecordFloatingWorkspaceV1InitializedHistogram();
    InitForV1();
    return;
  }

  if (version_ == floating_workspace_util::FloatingWorkspaceVersion::
                      kFloatingWorkspaceV2Enabled &&
      floating_workspace_util::IsFloatingWorkspaceV2Enabled()) {
    InitForV2(sync_service, desk_sync_service, device_info_sync_service);
  }
  LOG(WARNING) << "Floating workspace V2 init (not a warning)";
}

void FloatingWorkspaceService::SubscribeToForeignSessionUpdates() {
  syncer::SyncService* sync_service =
      SyncServiceFactory::GetForProfile(profile_);
  // If sync is disabled no need to observe anything.
  if (!sync_service || !sync_service->IsSyncFeatureEnabled()) {
    return;
  }
  foreign_session_updated_subscription_ =
      session_sync_service_->SubscribeToForeignSessionsChanged(
          base::BindRepeating(
              &FloatingWorkspaceService::
                  RestoreBrowserWindowsFromMostRecentlyUsedDevice,
              weak_pointer_factory_.GetWeakPtr()));
}

void FloatingWorkspaceService::
    RestoreBrowserWindowsFromMostRecentlyUsedDevice() {
  if (!should_run_restore_)
    return;
  if (base::TimeTicks::Now() >
      initialization_timeticks_ +
          ash::features::kFloatingWorkspaceMaxTimeAvailableForRestoreAfterLogin
              .Get()) {
    // No need to restore any remote session 3 seconds (TBD) after login.
    should_run_restore_ = false;
    return;
  }
  const sync_sessions::SyncedSession* most_recently_used_remote_session =
      GetMostRecentlyUsedRemoteSession();
  const sync_sessions::SyncedSession* local_session = GetLocalSession();
  if (!most_recently_used_remote_session ||
      (local_session &&
       local_session->GetModifiedTime() >
           most_recently_used_remote_session->GetModifiedTime())) {
    // If local session is the most recently modified or no remote session,
    // dispatch a delayed task to check whether any foreign session got updated.
    // If remote session is not updated after the delay, launch local session.
    base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
        FROM_HERE,
        base::BindOnce(
            &FloatingWorkspaceService::TryRestoreMostRecentlyUsedSession,
            weak_pointer_factory_.GetWeakPtr()),
        ash::features::kFloatingWorkspaceMaxTimeAvailableForRestoreAfterLogin
            .Get());
    should_run_restore_ = false;
    return;
  }

  // Restore most recently used remote session.
  RestoreForeignSessionWindows(most_recently_used_remote_session);
  should_run_restore_ = false;
}

void FloatingWorkspaceService::TryRestoreMostRecentlyUsedSession() {
  // A task generated by RestoreBrowserWindowsFromMostRecentlyUsedDevice
  // will call this method with a delay, at this time if local session is
  // still more recent, restore the local session.
  const sync_sessions::SyncedSession* local_session = GetLocalSession();
  const sync_sessions::SyncedSession* most_recently_used_remote_session =
      GetMostRecentlyUsedRemoteSession();
  if (local_session) {
    if (!most_recently_used_remote_session ||
        local_session->GetModifiedTime() >
            most_recently_used_remote_session->GetModifiedTime()) {
      // This is a delayed task, if at this time local session is still
      // most recent, restore local session.
      RestoreLocalSessionWindows();
    } else {
      RestoreForeignSessionWindows(most_recently_used_remote_session);
    }
  } else if (most_recently_used_remote_session) {
    RestoreForeignSessionWindows(most_recently_used_remote_session);
  }
}

void FloatingWorkspaceService::OnStateChanged(syncer::SyncService* sync) {
  OnNetworkStateOrSyncServiceStateChanged();
  // Prematurely return when sync feature is not active.
  if (!sync_service_->IsSyncFeatureActive()) {
    return;
  }
  if (!should_run_restore_) {
    MaybeSignOutOfCurrentSession();
    return;
  }
  if (download_status_cache_.has_value() &&
      download_status_cache_.value() ==
          sync->GetDownloadStatusFor(syncer::DataType::WORKSPACE_DESK)) {
    return;
  }
  download_status_cache_ =
      sync->GetDownloadStatusFor(syncer::DataType::WORKSPACE_DESK);
  switch (sync->GetDownloadStatusFor(syncer::DataType::WORKSPACE_DESK)) {
    case syncer::SyncService::DataTypeDownloadStatus::kWaitingForUpdates: {
      // Floating Workspace Service needs to wait until workspace desks are
      // up to date.
      break;
    }
    case syncer::SyncService::DataTypeDownloadStatus::kUpToDate: {
      if (!first_uptodate_download_timeticks_.has_value()) {
        first_uptodate_download_timeticks_ = base::TimeTicks::Now();
      }
      if (!is_cache_ready_) {
        should_launch_on_ready_ = true;
        VLOG(1) << "App cache is not ready. Don't restore floating "
                   "workspace yet.";
        return;
      }
      StopProgressBarAndRestoreFloatingWorkspace();
      break;
    }
    case syncer::SyncService::DataTypeDownloadStatus::kError: {
      // Sync is not expected to deliver the data, let user decide.
      // TODO: send notification to user asking if restore local.
      if (!should_run_restore_) {
        return;
      }
      StopProgressBarNotification();
      HandleSyncError();
      break;
    }
  }
}

void FloatingWorkspaceService::DefaultNetworkChanged(
    const NetworkState* network) {
  OnNetworkStateOrSyncServiceStateChanged();
}

void FloatingWorkspaceService::NetworkConnectionStateChanged(
    const NetworkState* network) {
  OnNetworkStateOrSyncServiceStateChanged();
}

void FloatingWorkspaceService::OnNetworkStateOrSyncServiceStateChanged() {
  if (!floating_workspace_util::IsInternetConnected() ||
      (sync_service_ && !sync_service_->IsSyncFeatureActive())) {
    // Only send notification if there's no notification currently or the
    // current notification is the same one that we want to display. If the
    // restore should not run, then there's no need to display the notification
    // either.
    if (should_run_restore_ &&
        (notification_ == nullptr ||
         notification_->id() != kNotificationForNoNetworkConnection)) {
      StopProgressBarNotification();
      SendNotification(kNotificationForNoNetworkConnection);
    }
  } else {
    if (notification_ != nullptr &&
        notification_->id() == kNotificationForNoNetworkConnection) {
      MaybeCloseNotification();
    }
  }
}
void FloatingWorkspaceService::Click(
    const std::optional<int>& button_index,
    const std::optional<std::u16string>& reply) {
  DCHECK(notification_);

  switch (GetNotificationTypeById(notification_->id())) {
    case FloatingWorkspaceServiceNotificationType::kUnknown:
      // For unknown type of notification id, do nothing and run close logic.
    case FloatingWorkspaceServiceNotificationType::kSyncErrorOrTimeOut:
    case FloatingWorkspaceServiceNotificationType::kProgressStatus:
    case FloatingWorkspaceServiceNotificationType::kSafeMode:
      break;
    case FloatingWorkspaceServiceNotificationType::kNoNetworkConnection:
      if (button_index.has_value()) {
        // Show network settings if the user clicks the show network settings
        // button.
        chrome::SettingsWindowManager::GetInstance()->ShowOSSettings(
            profile_, chromeos::settings::mojom::kNetworkSectionPath);
      }
      break;
    case FloatingWorkspaceServiceNotificationType::kRestoreAfterError:
      if (!button_index.has_value() ||
          button_index.value() ==
              static_cast<int>(
                  RestoreFromErrorNotificationButtonIndex::kRestore)) {
        VLOG(1) << "Restore button clicked for floating workspace after error";
        if (floating_workspace_template_to_restore_ != nullptr) {
          LaunchFloatingWorkspaceTemplate(
              floating_workspace_template_to_restore_.get());
        }
      }
      break;
  }
  MaybeCloseNotification();
}

void FloatingWorkspaceService::MaybeCloseNotification() {
  if (notification_ == nullptr) {
    return;
  }
  // If it's a progress bar notification and we're still waiting for chrome sync
  // to finish downloading, don't need to close notification.
  if (notification_->type() == message_center::NOTIFICATION_TYPE_PROGRESS &&
      !progress_notification_id_.empty() &&
      progress_notification_id_ == notification_->id()) {
    return;
  }
  auto* notification_display_service =
      NotificationDisplayService::GetForProfile(profile_);
  notification_display_service->Close(NotificationHandler::Type::TRANSIENT,
                                      notification_->id());
  notification_ = nullptr;
}

void FloatingWorkspaceService::SuspendImminent(
    power_manager::SuspendImminent::Reason reason) {
  timestamp_before_suspend_ = base::Time::Now();
}

void FloatingWorkspaceService::SuspendDone(base::TimeDelta sleep_duration) {
  restore_upon_wake_ = true;
}

void FloatingWorkspaceService::OnDeviceInfoChange() {}

void FloatingWorkspaceService::InitForV1() {
  session_sync_service_ =
      SessionSyncServiceFactory::GetInstance()->GetForProfile(profile_);
}

void FloatingWorkspaceService::InitForV2(
    syncer::SyncService* sync_service,
    desks_storage::DeskSyncService* desk_sync_service,
    syncer::DeviceInfoSyncService* device_info_sync_service) {
  // Disable floating workspace action in safe mode.
  if (floating_workspace_util::IsSafeMode()) {
    LOG(WARNING) << "Floating workspace disabled in safe mode.";
    SendNotification(kSafeMode);
    return;
  }
  floating_workspace_metrics_util::
      RecordFloatingWorkspaceV2InitializedHistogram();
  SetUpServiceAndObservers(sync_service, desk_sync_service,
                           device_info_sync_service);
  InitiateSigninTask();
}

const sync_sessions::SyncedSession*
FloatingWorkspaceService::GetMostRecentlyUsedRemoteSession() {
  sync_sessions::OpenTabsUIDelegate* open_tabs = GetOpenTabsUIDelegate();
  std::vector<raw_ptr<const sync_sessions::SyncedSession, VectorExperimental>>
      remote_sessions;
  if (!open_tabs || !open_tabs->GetAllForeignSessions(&remote_sessions)) {
    return nullptr;
  }
  // GetAllForeignSessions returns remote sessions in sorted way
  // with most recent at first.
  return remote_sessions.front();
}

const sync_sessions::SyncedSession*
FloatingWorkspaceService::GetLocalSession() {
  sync_sessions::OpenTabsUIDelegate* open_tabs = GetOpenTabsUIDelegate();
  const sync_sessions::SyncedSession* local_session = nullptr;
  if (!open_tabs || !open_tabs->GetLocalSession(&local_session))
    return nullptr;
  return local_session;
}

void FloatingWorkspaceService::RestoreForeignSessionWindows(
    const sync_sessions::SyncedSession* session) {
  sync_sessions::OpenTabsUIDelegate* open_tabs = GetOpenTabsUIDelegate();
  if (!open_tabs) {
    return;
  }
  std::vector<const sessions::SessionWindow*> session_windows =
      open_tabs->GetForeignSession(session->GetSessionTag());
  if (session_windows.empty()) {
    return;
  }
  SessionRestore::RestoreForeignSessionWindows(
      profile_, session_windows.begin(), session_windows.end());
  floating_workspace_metrics_util::RecordFloatingWorkspaceV1RestoredSessionType(
      floating_workspace_metrics_util::RestoredBrowserSessionType::kRemote);
}

void FloatingWorkspaceService::RestoreLocalSessionWindows() {
  // Restore local session based on user settings in
  // chrome://settings/onStartup.
  UserSessionManager::GetInstance()->LaunchBrowser(profile_);
  floating_workspace_metrics_util::RecordFloatingWorkspaceV1RestoredSessionType(
      floating_workspace_metrics_util::RestoredBrowserSessionType::kLocal);
}

sync_sessions::OpenTabsUIDelegate*
FloatingWorkspaceService::GetOpenTabsUIDelegate() {
  DCHECK(session_sync_service_);
  return session_sync_service_->GetOpenTabsUIDelegate();
}

void FloatingWorkspaceService::StartCaptureAndUploadActiveDesk() {
  CaptureAndUploadActiveDesk();
  if (!timer_.IsRunning()) {
    timer_.Start(
        FROM_HERE,
        ash::features::kFloatingWorkspaceV2PeriodicJobIntervalInSeconds.Get(),
        this, &FloatingWorkspaceService::CaptureAndUploadActiveDesk);
  }
}

void FloatingWorkspaceService::StopCaptureAndUploadActiveDesk() {
  if (timer_.IsRunning()) {
    timer_.Stop();
  }
}

void FloatingWorkspaceService::MaybeStartProgressBarNotification() {
  if (!should_run_restore_) {
    return;
  }
  progress_timer_.Start(FROM_HERE, kProgressTimeUpdateDelay, this,
                        &FloatingWorkspaceService::HandleProgressBarStatus);
}

void FloatingWorkspaceService::StopProgressBarNotification() {
  progress_notification_id_ = std::string();
  if (progress_timer_.IsRunning()) {
    progress_timer_.Stop();
  }
  MaybeCloseNotification();
}

void FloatingWorkspaceService::HandleProgressBarStatus() {
  const base::TimeDelta time_difference =
      base::TimeTicks::Now() - initialization_timeticks_;
  if (!should_run_restore_ ||
      time_difference >=
          ash::features::
              kFloatingWorkspaceV2MaxTimeAvailableForRestoreAfterLogin.Get()) {
    StopProgressBarNotification();
    MaybeHandleDownloadTimeOut();
    return;
  }

  SendNotification(kNotificationForProgressStatus);
}

bool FloatingWorkspaceService::ShouldExcludeTemplate(
    const DeskTemplate* floating_workspace_template) {
  if (!floating_workspace_template) {
    return true;
  }
  // We only consider remote entries if the device has woken up from suspend and
  // we are going through a restore flow. In this case, we only want to consider
  // restoring floating workspaces from other devices and if those templates
  // were after the device went to suspend mode. `timestamp_before_suspend_` is
  // only set if the user has gone to suspend, otherwise it is a nullopt.
  // Therefore, if it is set and we're getting entries for restore, we only want
  // to consider remote entries.
  bool should_only_consider_remote_entries =
      timestamp_before_suspend_.has_value();
  if (!should_only_consider_remote_entries) {
    return false;
  }
  bool is_remote_entry = floating_workspace_template->client_cache_guid() !=
                         desk_sync_service_->GetDeskModel()->GetCacheGuid();
  bool is_uploaded_after_suspend =
      floating_workspace_template->GetLastUpdatedTime() >
      timestamp_before_suspend_.value();
  return !(is_remote_entry && is_uploaded_after_suspend);
}

const DeskTemplate*
FloatingWorkspaceService::GetLatestFloatingWorkspaceTemplate() {
  const DeskTemplate* floating_workspace_template = nullptr;
  std::vector<const ash::DeskTemplate*> fws_entries =
      GetFloatingWorkspaceTemplateEntries();
  VLOG(1) << "Found " << fws_entries.size() << " floating workspace entries";
  for (const DeskTemplate* entry : fws_entries) {
    if (ShouldExcludeTemplate(entry)) {
      continue;
    }
    if (!floating_workspace_template ||
        floating_workspace_template->GetLastUpdatedTime() <
            entry->GetLastUpdatedTime()) {
      floating_workspace_template = entry;
    }
  }
  DoGarbageCollection(/*exclude template=*/floating_workspace_template);
  return floating_workspace_template;
}

std::vector<const ash::DeskTemplate*>
FloatingWorkspaceService::GetFloatingWorkspaceTemplateEntries() {
  std::vector<const ash::DeskTemplate*> entries;
  if (!desk_sync_service_ || !desk_sync_service_->GetDeskModel()) {
    return entries;
  }
  desks_storage::DeskModel::GetAllEntriesResult result =
      desk_sync_service_->GetDeskModel()->GetAllEntries();
  if (result.status != desks_storage::DeskModel::GetAllEntriesStatus::kOk) {
    return entries;
  }
  for (const DeskTemplate* desk_template : result.entries) {
    if (desk_template &&
        desk_template->type() == DeskTemplateType::kFloatingWorkspace) {
      entries.push_back(desk_template);
    }
  }
  return entries;
}

void FloatingWorkspaceService::CaptureAndUploadActiveDesk() {
  GetDesksClient()->CaptureActiveDesk(
      base::BindOnce(&FloatingWorkspaceService::OnTemplateCaptured,
                     weak_pointer_factory_.GetWeakPtr()),
      DeskTemplateType::kFloatingWorkspace);
}

void FloatingWorkspaceService::CaptureAndUploadActiveDeskForTest(
    std::unique_ptr<DeskTemplate> desk_template) {
  OnTemplateCaptured(std::nullopt, std::move(desk_template));
}

void FloatingWorkspaceService::StopProgressBarAndRestoreFloatingWorkspace() {
  StopProgressBarNotification();
  RestoreFloatingWorkspaceTemplate(GetLatestFloatingWorkspaceTemplate());
  StartCaptureAndUploadActiveDesk();
}

void FloatingWorkspaceService::RestoreFloatingWorkspaceTemplate(
    const DeskTemplate* desk_template) {
  if (desk_template == nullptr) {
    LOG(WARNING)
        << "No floating workspace entry found. Won't "
           "restore. This is only possible if this is the first time "
           "a user is using Floating Workspace or we are attempting to restore "
           "from a suspend mode and there are no remote entries to restore.";
    should_run_restore_ = false;
    floating_workspace_metrics_util::
        RecordFloatingWorkspaceV2TemplateNotFound();
    return;
  }
  // Record metrics for window and tab count and also the time it took to
  // download the floating workspace template.
  floating_workspace_metrics_util::RecordFloatingWorkspaceV2TemplateLoadTime(
      base::TimeTicks::Now() - initialization_timeticks_);
  RecordWindowAndTabCountHistogram(*desk_template);
  // Check if template has been downloaded after
  // kFloatingWorkspaceV2MaxTimeAvailableForRestoreAfterLogin.
  if (base::TimeTicks::Now() >
      initialization_timeticks_ +
          ash::features::
              kFloatingWorkspaceV2MaxTimeAvailableForRestoreAfterLogin.Get()) {
    // Template arrives late, asking user to restore or not.
    StopProgressBarNotification();
    SendNotification(kNotificationForRestoreAfterError);
    // Save the workspace template in memory so we can restore the correct one.
    floating_workspace_template_to_restore_ = desk_template->Clone();
    // Set this flag to false after sending restore notification to user
    // since the user will control the restoration behavior from here on.
    should_run_restore_ = false;
    return;
  }
  LaunchFloatingWorkspaceTemplate(desk_template);
}

void FloatingWorkspaceService::LaunchFloatingWorkspaceTemplate(
    const DeskTemplate* desk_template) {
  should_run_restore_ = false;
  if (desk_template == nullptr) {
    return;
  }
  base::Uuid active_desk_uuid = GetDesksClient()->GetActiveDesk();
  VLOG(1) << "Launching Floating Workspace template with timestamp of "
          << desk_template->GetLastUpdatedTime();
  RemoveAllPreviousDesksExceptActiveDesk(
      /*exclude_desk_uuid=*/active_desk_uuid);

  // Close all windows between waking up from sleep and restore operation.
  // TODO: b/331420684 - Remove apps and windows in place without having to
  // launch a new desk.
  if (launch_on_new_desk_) {
    GetDesksClient()->LaunchDeskTemplate(
        desk_template->uuid(),
        base::BindOnce(&FloatingWorkspaceService::OnTemplateLaunched,
                       weak_pointer_factory_.GetWeakPtr()),
        desk_template->template_name());
    return;
  }
  VLOG(1) << "Combining Floating Workspace apps to current desk.";
  std::unique_ptr<DeskTemplate> template_copy = desk_template->Clone();
  // Open the apps from the floating workspace on top of existing windows.
  saved_desk_util::UpdateTemplateActivationIndicesRelativeOrder(*template_copy);
  GetDesksClient()->LaunchAppsFromTemplate(std::move(template_copy));
  RecordLaunchSavedDeskHistogram(DeskTemplateType::kFloatingWorkspace);
}

void FloatingWorkspaceService::OnTemplateLaunched(
    std::optional<DesksClient::DeskActionError> error,
    const base::Uuid& desk_uuid) {
  if (error) {
    HandleTemplateLaunchErrors(error.value());
    return;
  }
  RecordLaunchSavedDeskHistogram(DeskTemplateType::kFloatingWorkspace);
  RemoveAllPreviousDesksExceptActiveDesk(/*exclude_desk_uuid=*/desk_uuid);
}

DesksClient* FloatingWorkspaceService::GetDesksClient() {
  return DesksClient::Get();
}

bool FloatingWorkspaceService::IsCurrentDeskSameAsPrevious(
    DeskTemplate* current_desk_template) const {
  if (!previously_captured_desk_template_) {
    return false;
  }

  // If the last user activity was before the last uploaded template, then it is
  // very likely that the current captured desk is done due to changing urls for
  // the same window (caused by things like auth protection on gmail app when
  // certs aren't installed).
  if (ui::UserActivityDetector::Get()->last_activity_time() <=
      last_uploaded_timeticks_) {
    return true;
  }

  const auto& previous_app_id_to_app_launch_list =
      previously_captured_desk_template_->desk_restore_data()
          ->app_id_to_launch_list();
  const auto& current_app_id_to_app_launch_list =
      current_desk_template->desk_restore_data()->app_id_to_launch_list();

  // If previous and current template have different number of apps they are
  // different.
  if (previous_app_id_to_app_launch_list.size() !=
      current_app_id_to_app_launch_list.size()) {
    return false;
  }

  for (const auto& it : previous_app_id_to_app_launch_list) {
    const std::string app_id = it.first;
    // Cannot find app id in currently captured desk.
    if (current_app_id_to_app_launch_list.find(app_id) ==
        current_app_id_to_app_launch_list.end()) {
      return false;
    }
    for (const auto& [restore_window_id, previous_app_restore_data] :
         it.second) {
      auto& current_app_restore_data_launch_list =
          current_app_id_to_app_launch_list.at(app_id);
      // Cannot find window id in currently captured template.
      if (current_app_restore_data_launch_list.find(restore_window_id) ==
          current_app_restore_data_launch_list.end()) {
        return false;
      }
      // For the same window the data inside are different.
      if (*current_app_restore_data_launch_list.at(restore_window_id) !=
          *previous_app_restore_data) {
        return false;
      }
    }
  }
  return true;
}

void FloatingWorkspaceService::HandleTemplateCaptureErrors(
    DesksClient::DeskActionError error) {
  switch (error) {
    case DesksClient::DeskActionError::kUnknownError:
      LOG(WARNING) << "Failed to capture template: unknown error.";
      return;
    case DesksClient::DeskActionError::kStorageError:
      LOG(WARNING) << "Failed to capture template: storage error.";
      return;
    case DesksClient::DeskActionError::kDesksCountCheckFailedError:
      LOG(WARNING) << "Failed to capture template: max number of desks open.";
      return;
    case DesksClient::DeskActionError::kNoCurrentUserError:
      LOG(WARNING) << "Failed to capture template: no active user.";
      return;
    case DesksClient::DeskActionError::kBadProfileError:
      LOG(WARNING) << "Failed to capture template: bad profile.";
      return;
    case DesksClient::DeskActionError::kResourceNotFoundError:
      LOG(WARNING) << "Failed to capture template: resource not found.";
      return;
    case DesksClient::DeskActionError::kInvalidIdError:
      LOG(WARNING) << "Failed to capture template: desk id is invalid.";
      return;
    case DesksClient::DeskActionError::kDesksBeingModifiedError:
      LOG(WARNING)
          << "Failed to capture template: desk is currently being modified.";
      return;
  }
}

void FloatingWorkspaceService::HandleTemplateLaunchErrors(
    DesksClient::DeskActionError error) {
  switch (error) {
    case DesksClient::DeskActionError::kUnknownError:
      floating_workspace_metrics_util::
          RecordFloatingWorkspaceV2TemplateLaunchFailureType(
              floating_workspace_metrics_util::LaunchTemplateFailureType::
                  kUnknownError);
      LOG(WARNING) << "Failed to launch template: unknown error.";
      return;
    case DesksClient::DeskActionError::kStorageError:
      floating_workspace_metrics_util::
          RecordFloatingWorkspaceV2TemplateLaunchFailureType(
              floating_workspace_metrics_util::LaunchTemplateFailureType::
                  kStorageError);
      LOG(WARNING) << "Failed to launch template: storage error.";
      return;
    case DesksClient::DeskActionError::kDesksCountCheckFailedError:
      floating_workspace_metrics_util::
          RecordFloatingWorkspaceV2TemplateLaunchFailureType(
              floating_workspace_metrics_util::LaunchTemplateFailureType::
                  kDesksCountCheckFailedError);
      LOG(WARNING) << "Failed to launch template: max number of desks open.";
      return;
    // No need to record metrics for the below desk action errors since they
    // do not relate to template launch.
    case DesksClient::DeskActionError::kNoCurrentUserError:
      LOG(WARNING) << "Failed to launch template: no active user.";
      return;
    case DesksClient::DeskActionError::kBadProfileError:
      LOG(WARNING) << "Failed to launch template: bad profile.";
      return;
    case DesksClient::DeskActionError::kResourceNotFoundError:
      LOG(WARNING) << "Failed to launch template: resource not found.";
      return;
    case DesksClient::DeskActionError::kInvalidIdError:
      LOG(WARNING) << "Failed to launch template: desk id is invalid.";
      return;
    case DesksClient::DeskActionError::kDesksBeingModifiedError:
      LOG(WARNING)
          << "Failed to launch template: desk is currently being modified.";
      return;
  }
}

void FloatingWorkspaceService::OnTemplateCaptured(
    std::optional<DesksClient::DeskActionError> error,
    std::unique_ptr<DeskTemplate> desk_template) {
  // Desk capture was not successful, nothing to upload.
  if (error) {
    HandleTemplateCaptureErrors(error.value());
  }
  if (!desk_template) {
    LOG(WARNING) << "Desk capture failed. Nothing to upload.";
    return;
  }
  // Check if there's an associated floating workspace uuid from the desk
  // sync bridge. If there is, use that one. The
  // `floating_workspace_uuid_ is populated once during the first capture
  // of the session if there is known information from the sync bridge
  // and the info may be outdated for the sync bridge. However, the sync
  // bridge does not need to know the new uuid since the current service
  // will handle it. Ignore for testing.
  if (!floating_workspace_uuid_.has_value()) {
    std::optional<base::Uuid> floating_workspace_uuid_from_desk_model =
        GetFloatingWorkspaceUuidForCurrentDevice();
    if (floating_workspace_uuid_from_desk_model.has_value()) {
      floating_workspace_uuid_ =
          floating_workspace_uuid_from_desk_model.value();
    }
  }
  if (floating_workspace_uuid_.has_value() &&
      floating_workspace_uuid_.value().is_valid()) {
    desk_template->set_uuid(floating_workspace_uuid_.value());
  } else {
    floating_workspace_uuid_ = desk_template->uuid();
  }
  // If it successfully captured desk, remove old entry and record new uuid only
  // if the user was active from when the sync cycle is finished to now.
  if (!IsCurrentDeskSameAsPrevious(desk_template.get()) &&
      (first_uptodate_download_timeticks_.has_value() &&
       first_uptodate_download_timeticks_.value() <=
           ui::UserActivityDetector::Get()->last_activity_time())) {
    UploadFloatingWorkspaceTemplateToDeskModel(std::move(desk_template));
  }
}

void FloatingWorkspaceService::UploadFloatingWorkspaceTemplateToDeskModel(
    std::unique_ptr<DeskTemplate> desk_template) {
  // Upload and save the template.
  auto* active_user = user_manager::UserManager::Get()->GetActiveUser();
  auto* user_profile = ProfileHelper::Get()->GetProfileByUser(active_user);
  // Do not upload if the active user profile doesn't match the logged in user
  // profile.
  if (user_profile != profile_) {
    return;
  }
  desk_sync_service_->GetDeskModel()->AddOrUpdateEntry(
      std::move(desk_template),
      base::BindOnce(&FloatingWorkspaceService::OnTemplateUploaded,
                     weak_pointer_factory_.GetWeakPtr()));
}

void FloatingWorkspaceService::OnTemplateUploaded(
    desks_storage::DeskModel::AddOrUpdateEntryStatus status,
    std::unique_ptr<DeskTemplate> new_entry) {
  previously_captured_desk_template_ = std::move(new_entry);
  last_uploaded_timeticks_ = base::TimeTicks::Now();
  floating_workspace_metrics_util::
      RecordFloatingWorkspaceV2TemplateUploadStatusHistogram(status);
  VLOG(1) << "Desk template uploaded successfully.";
}

std::optional<base::Uuid>
FloatingWorkspaceService::GetFloatingWorkspaceUuidForCurrentDevice() {
  std::string cache_guid = desk_sync_service_->GetDeskModel()->GetCacheGuid();
  std::vector<const ash::DeskTemplate*> fws_entries =
      GetFloatingWorkspaceTemplateEntries();
  for (const DeskTemplate* entry : fws_entries) {
    if (entry && entry->client_cache_guid() == cache_guid) {
      return entry->uuid();
    }
  }
  return std::nullopt;
}

void FloatingWorkspaceService::HandleSyncError() {
  SendNotification(kNotificationForSyncErrorOrTimeOut);
}

void FloatingWorkspaceService::MaybeHandleDownloadTimeOut() {
  if (download_status_cache_.has_value() &&
      download_status_cache_.value() ==
          syncer::SyncService::DataTypeDownloadStatus::kUpToDate) {
    should_run_restore_ = false;
  }
  if (!should_run_restore_) {
    return;
  }
  // Record timeout metrics.
  floating_workspace_metrics_util::
      RecordFloatingWorkspaceV2TemplateLaunchTimeout(
          floating_workspace_metrics_util::LaunchTemplateTimeoutType::
              kPassedWaitPeriod);
  SendNotification(kNotificationForSyncErrorOrTimeOut);
}

void FloatingWorkspaceService::SendNotification(const std::string& id) {
  // If there is a previous notification for floating workspace, close it.
  MaybeCloseNotification();

  message_center::RichNotificationData notification_data;
  std::u16string title, message;
  message_center::SystemNotificationWarningLevel warning_level;
  const base::TimeDelta time_difference =
      base::TimeTicks::Now() - initialization_timeticks_;
  bool is_progress_bar = false;
  switch (GetNotificationTypeById(id)) {
    case FloatingWorkspaceServiceNotificationType::kNoNetworkConnection:
      title =
          l10n_util::GetStringUTF16(IDS_FLOATING_WORKSPACE_NO_NETWORK_TITLE);
      message =
          l10n_util::GetStringUTF16(IDS_FLOATING_WORKSPACE_NO_NETWORK_MESSAGE);
      warning_level =
          message_center::SystemNotificationWarningLevel::CRITICAL_WARNING;
      notification_data.buttons.emplace_back(
          l10n_util::GetStringUTF16(IDS_FLOATING_WORKSPACE_NO_NETWORK_BUTTON));
      break;
    case FloatingWorkspaceServiceNotificationType::kSyncErrorOrTimeOut:
      title = l10n_util::GetStringUTF16(IDS_FLOATING_WORKSPACE_ERROR_TITLE);
      message = l10n_util::GetStringUTF16(IDS_FLOATING_WORKSPACE_ERROR_MESSAGE);
      warning_level =
          message_center::SystemNotificationWarningLevel::CRITICAL_WARNING;
      break;
    case FloatingWorkspaceServiceNotificationType::kRestoreAfterError:
      title = l10n_util::GetStringUTF16(
          IDS_FLOATING_WORKSPACE_RESTORE_FROM_ERROR_TITLE);
      message = l10n_util::GetStringUTF16(
          IDS_FLOATING_WORKSPACE_RESTORE_FROM_ERROR_MESSAGE);
      warning_level = message_center::SystemNotificationWarningLevel::NORMAL;
      notification_data.buttons.emplace_back(l10n_util::GetStringUTF16(
          IDS_FLOATING_WORKSPACE_RESTORE_FROM_ERROR_RESTORATION_BUTTON));
      break;
    case FloatingWorkspaceServiceNotificationType::kProgressStatus:
      title =
          l10n_util::GetStringUTF16(IDS_FLOATING_WORKSPACE_PROGRESS_BAR_TITLE);
      notification_data.progress_status = l10n_util::GetStringUTF16(
          IDS_FLOATING_WORKSPACE_PROGRESS_BAR_MESSAGE);
      warning_level = message_center::SystemNotificationWarningLevel::NORMAL;
      notification_data.progress = std::min(
          100.0,
          (time_difference * 100.0) /
              ash::features::
                  kFloatingWorkspaceV2MaxTimeAvailableForRestoreAfterLogin
                      .Get());
      is_progress_bar = true;
      break;
    case FloatingWorkspaceServiceNotificationType::kSafeMode:
      title = l10n_util::GetStringUTF16(IDS_FLOATING_WORKSPACE_SAFE_MODE_TITLE);
      message =
          l10n_util::GetStringUTF16(IDS_FLOATING_WORKSPACE_SAFE_MODE_MESSAGE);
      warning_level = message_center::SystemNotificationWarningLevel::WARNING;
      break;
    case FloatingWorkspaceServiceNotificationType::kUnknown:
      VLOG(2) << "Unknown notification type for floating workspace, skip "
                 "sending notification";
      return;
  }
  // Update the current notification with progress status if we are still
  // showing progress status. Otherwise, make a new notification.
  if (is_progress_bar && notification_ != nullptr &&
      !progress_notification_id_.empty() &&
      notification_->id() == progress_notification_id_) {
    notification_->set_progress(notification_data.progress);
  } else {
    notification_ = CreateSystemNotificationPtr(
        is_progress_bar ? message_center::NOTIFICATION_TYPE_PROGRESS
                        : message_center::NOTIFICATION_TYPE_SIMPLE,
        id, title, message,
        l10n_util::GetStringUTF16(IDS_FLOATING_WORKSPACE_DISPLAY_SOURCE),
        GURL(),
        message_center::NotifierId(
            message_center::NotifierType::SYSTEM_COMPONENT, id,
            NotificationCatalogName::kFloatingWorkspace),
        notification_data,
        base::MakeRefCounted<message_center::ThunkNotificationDelegate>(
            weak_pointer_factory_.GetWeakPtr()),
        kFloatingWorkspaceNotificationIcon, warning_level);
    notification_->set_priority(message_center::SYSTEM_PRIORITY);
    if (is_progress_bar) {
      progress_notification_id_ = notification_->id();
    }
  }
  auto* notification_display_service =
      NotificationDisplayService::GetForProfile(profile_);
  notification_display_service->Display(NotificationHandler::Type::TRANSIENT,
                                        *notification_,
                                        /*metadata=*/nullptr);
}

void FloatingWorkspaceService::DoGarbageCollection(
    const DeskTemplate* exclude_template) {
  // Do not delete any floating workspace templates if we have less than 2
  // templates. We want to keep the latest template. If there's only one
  // floating workspace template then this is the latest one.
  std::vector<const DeskTemplate*> fws_entries =
      GetFloatingWorkspaceTemplateEntries();
  if (fws_entries.size() < 2) {
    return;
  }
  for (const DeskTemplate* entry : fws_entries) {
    const base::TimeDelta template_age =
        base::Time::Now() - entry->GetLastUpdatedTime();
    if (template_age < kStaleFWSThreshold ||
        (exclude_template != nullptr &&
         exclude_template->uuid() == entry->uuid())) {
      continue;
    }
    base::Uuid uuid = entry->uuid();
    desk_sync_service_->GetDeskModel()->DeleteEntry(uuid, base::DoNothing());
  }
}

// TODO(b/294456894): Migrate to desk controller logic.
void FloatingWorkspaceService::RemoveAllPreviousDesksExceptActiveDesk(
    const base::Uuid& exclude_desk_uuid) {
  auto all_desks = GetDesksClient()->GetAllDesks();
  if (all_desks.has_value() && all_desks.value().size() > 1) {
    for (const Desk* entry : all_desks.value()) {
      if (entry && entry->uuid() != exclude_desk_uuid) {
        base::Uuid uuid_to_remove = entry->uuid();
        GetDesksClient()->RemoveDesk(uuid_to_remove,
                                     ash::DeskCloseType::kCloseAllWindows);
      }
    }
  }
}

void FloatingWorkspaceService::MaybeSignOutOfCurrentSession() {
  base::TimeDelta time_delta =
      ui::UserActivityDetector::Get()->last_activity_time() -
      initialization_timeticks_;
  if (sync_service_->GetDownloadStatusFor(syncer::DataType::DEVICE_INFO) ==
      syncer::SyncService::DataTypeDownloadStatus::kUpToDate) {
    std::vector<const syncer::DeviceInfo*> all_devices =
        device_info_sync_service_->GetDeviceInfoTracker()->GetAllDeviceInfo();

    // Sort the DeviceInfo vector so the most recently modified devices are
    // first.
    std::sort(
        all_devices.begin(), all_devices.end(),
        [](const syncer::DeviceInfo* device1,
           const syncer::DeviceInfo* device2) {
          return device1->floating_workspace_last_signin_timestamp().value_or(
                     base::Time()) >
                 device2->floating_workspace_last_signin_timestamp().value_or(
                     base::Time());
        });
    // Checks if the most recently modified devices are after this device's last
    // active timestamp.
    for (const syncer::DeviceInfo* device : all_devices) {
      // If the timestamp is older than the current timestamp or the entry is
      // nullopt, then any other devices afterwards are older, so we can stop
      // here.
      if (!device->floating_workspace_last_signin_timestamp().has_value() ||
          device->floating_workspace_last_signin_timestamp().value() <
              initialization_time_ +
                  (time_delta.is_positive() ? time_delta : base::Seconds(0)) +
                  kMinTimeToWait) {
        break;
      }
      // Skip current device info.
      if (device_info_sync_service_->GetDeviceInfoTracker()
              ->IsRecentLocalCacheGuid(device->guid())) {
        continue;
      }
      // We log out if we reach this part of the loop. We only reach here when:
      // 1) the device info is not for the current device and 2) the last active
      // timestamp is after the last user activity on this device.
      ash::Shell::Get()->session_controller()->RequestSignOut();
    }
  }

  // As a final resort, if we could not logout via the device info, or
  // floating workspace entries came first before the device info, use
  // floating workspace entries to determine if we should logout.
  if (sync_service_->GetDownloadStatusFor(syncer::DataType::WORKSPACE_DESK) !=
      syncer::SyncService::DataTypeDownloadStatus::kUpToDate) {
    return;
  }
  auto* latest_floating_workspace = GetLatestFloatingWorkspaceTemplate();

  if (latest_floating_workspace == nullptr) {
    return;
  }
  // Checks if the latest uploaded floating workspace template is a captured
  // template from this device and sign out of this session if it is not.
  // Note: we are comparing the last activity time for the user here with the
  // template that we just got. Since `last_activity_time` is in timeticks and
  // the template time is in time, we need to do some manually conversion with
  // Time. Note: this time_delta is strictly > 0 but can be smaller than wall
  // clock time difference. Some additional time buffer (using the 30s from
  // the periodic capture job) is added to account for clock drifts from
  // device to device.
  if (latest_floating_workspace->client_cache_guid() !=
          desk_sync_service_->GetDeskModel()->GetCacheGuid() &&
      latest_floating_workspace->GetLastUpdatedTime() >
          initialization_time_ +
              (time_delta.is_positive() ? time_delta : base::Seconds(0)) +
              ash::features::kFloatingWorkspaceV2PeriodicJobIntervalInSeconds
                  .Get()) {
    ash::Shell::Get()->session_controller()->RequestSignOut();
  }
}

void FloatingWorkspaceService::OnAppRegistryCacheWillBeDestroyed(
    apps::AppRegistryCache* cache) {
  // Set the cache readiness to false. If this is happening, then it's very
  // likely the service will be destroyed soon.
  is_cache_ready_ = false;
  app_cache_obs_.Reset();
}

bool FloatingWorkspaceService::AreRequiredAppTypesInitialized() {
  if (!app_cache_obs_.IsObserving()) {
    return false;
  }
  apps::AppRegistryCache* cache =
      apps::AppRegistryCacheWrapper::Get().GetAppRegistryCache(
          multi_user_util::GetAccountIdFromProfile(profile_));
  DCHECK(cache);
  const std::set<apps::AppType>& initialized_types =
      cache->InitializedAppTypes();
  if (!initialized_types.contains(apps::AppType::kWeb)) {
    return false;
  }
#if BUILDFLAG(IS_CHROMEOS_LACROS)
  return (
      initialized_types.contains(apps::AppType::kStandaloneBrowser) &&
      initialized_types.contains(apps::AppType::kStandaloneBrowserChromeApp));
#endif
#if BUILDFLAG(IS_CHROMEOS_ASH)
  return initialized_types.contains(apps::AppType::kChromeApp);
#endif
}

void FloatingWorkspaceService::OnAppTypeInitialized(apps::AppType app_type) {
  // If the cache is already ready we don't need to check for additional app
  // type initialization.
  if (is_cache_ready_) {
    return;
  }
  is_cache_ready_ = AreRequiredAppTypesInitialized();
  // If we're here it means that we have floating workspace template to be
  // launched, but until this point the AppRegistryCache wasn't ready.
  if (is_cache_ready_ && should_launch_on_ready_ && should_run_restore_) {
    StopProgressBarAndRestoreFloatingWorkspace();
  }
}

void FloatingWorkspaceService::OnAppRegistryCacheAdded(
    const AccountId& account_id) {
  if (account_id != multi_user_util::GetAccountIdFromProfile(profile_) ||
      app_cache_obs_.IsObserving()) {
    return;
  }
  auto* apps_cache =
      apps::AppRegistryCacheWrapper::Get().GetAppRegistryCache(account_id);
  app_cache_obs_.Observe(apps_cache);
  is_cache_ready_ = AreRequiredAppTypesInitialized();
}

void FloatingWorkspaceService::OnActiveUserSessionChanged(
    const AccountId& account_id) {
  VLOG(1) << "Active User session changed for fws";
  Profile* active_profile =
      ash::ProfileHelper::Get()->GetProfileByAccountId(account_id);
  // Stop the capture if the switched user is not the profile we logged in with.
  // Set up the observers again if we switched back to the profile we logged in
  // with.
  if (active_profile != profile_) {
    ShutDownServicesAndObservers();
  } else {
    SetUpServiceAndObservers(
        SyncServiceFactory::GetForProfile(profile_),
        DeskSyncServiceFactory::GetForProfile(profile_),
        DeviceInfoSyncServiceFactory::GetForProfile(profile_));
  }
}

void FloatingWorkspaceService::OnLockStateChanged(bool locked) {
  // The user has signed in the device via the lock screen and has woken up from
  // sleep mode. Reset initialization times and start the flow as if the user
  // has just logged in.
  if (!locked && restore_upon_wake_) {
    should_run_restore_ = true;
    launch_on_new_desk_ = true;
    restore_upon_wake_ = false;
    initialization_time_ = base::Time::Now();
    initialization_timeticks_ = base::TimeTicks::Now();
    InitiateSigninTask();
  }
}

void FloatingWorkspaceService::OnFocusLeavingSystemTray(bool reverse) {}

void FloatingWorkspaceService::OnSystemTrayBubbleShown() {
  CaptureAndUploadActiveDesk();
}

void FloatingWorkspaceService::OnLocalDeviceInfoProviderReady() {
  base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE,
      base::BindOnce(&FloatingWorkspaceService::UpdateLocalDeviceInfo,
                     weak_pointer_factory_.GetWeakPtr()));
}

void FloatingWorkspaceService::UpdateLocalDeviceInfo() {
  if (!device_info_sync_service_ ||
      !device_info_sync_service_->GetLocalDeviceInfoProvider() ||
      !device_info_sync_service_->GetLocalDeviceInfoProvider()
           ->GetLocalDeviceInfo()) {
    return;
  }
  syncer::LocalDeviceInfoProviderImpl* local_device_info_provider =
      static_cast<syncer::LocalDeviceInfoProviderImpl*>(
          device_info_sync_service_->GetLocalDeviceInfoProvider());
  local_device_info_provider->UpdateRecentSignInTime(initialization_time_);
  device_info_sync_service_->RefreshLocalDeviceInfo();
}

void FloatingWorkspaceService::ShutDownServicesAndObservers() {
  // Remove `this` service as an observer so we do not run into an issue where
  // chrome sync data is downloaded and the capture is kicked started after we
  // stopped the capture timer below.
  OnSyncShutdown(sync_service_);
  OnShuttingDown();
  OnDeviceInfoShutdown();
  // If we don't have an apps cache then we observe the wrapper to
  // wait for it to be ready.
  if (app_cache_obs_.IsObserving()) {
    app_cache_obs_.Reset();
  }
  if (app_cache_wrapper_obs_.IsObserving()) {
    app_cache_wrapper_obs_.Reset();
  }
  StopCaptureAndUploadActiveDesk();
  if (Shell::HasInstance() && Shell::Get()->system_tray_notifier()) {
    Shell::Get()->system_tray_notifier()->RemoveSystemTrayObserver(this);
  }
  if (chromeos::PowerManagerClient::Get()) {
    chromeos::PowerManagerClient::Get()->RemoveObserver(this);
  }
}

void FloatingWorkspaceService::SetUpServiceAndObservers(
    syncer::SyncService* sync_service,
    desks_storage::DeskSyncService* desk_sync_service,
    syncer::DeviceInfoSyncService* device_info_sync_service) {
  sync_service_ = sync_service;
  desk_sync_service_ = desk_sync_service;
  device_info_sync_service_ = device_info_sync_service;
  if (ash::NetworkHandler::IsInitialized()) {
    auto* network_handler = NetworkHandler::Get();
    if (!network_handler->network_state_handler()->HasObserver(this)) {
      network_handler->network_state_handler()->AddObserver(this);
    }
  }
  if (Shell::HasInstance() && Shell::Get()->system_tray_notifier()) {
    Shell::Get()->system_tray_notifier()->AddSystemTrayObserver(this);
  }
  if (sync_service_ && !sync_service_->HasObserver(this)) {
    sync_service_->AddObserver(this);
  }
  if (chromeos::PowerManagerClient::Get()) {
    chromeos::PowerManagerClient::Get()->AddObserver(this);
  }
  if (device_info_sync_service_ &&
      device_info_sync_service_->GetDeviceInfoTracker()) {
    device_info_sync_service_->GetDeviceInfoTracker()->AddObserver(this);
  }
  // If we don't have an apps cache then we observe the wrapper to
  // wait for it to be ready.
  auto& apps_cache_wrapper = apps::AppRegistryCacheWrapper::Get();
  DCHECK(&apps_cache_wrapper);
  auto* apps_cache = apps_cache_wrapper.GetAppRegistryCache(
      multi_user_util::GetAccountIdFromProfile(profile_));
  if (apps_cache) {
    app_cache_obs_.Observe(apps_cache);
  } else {
    app_cache_wrapper_obs_.Observe(&apps_cache_wrapper);
  }
  is_cache_ready_ = AreRequiredAppTypesInitialized();
  // Explicitly start the capture if we do not need to run restore. This means
  // we had already gone through the restore logic before a profile switch and
  // won't go through the restore procedure to start the capture. So instead,
  // just start capturing.
  if (!should_run_restore_) {
    StartCaptureAndUploadActiveDesk();
  }
}
}  // namespace ash