chromium/chrome/browser/ash/crostini/crostini_util.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 "chrome/browser/ash/crostini/crostini_util.h"

#include <utility>

#include "ash/constants/ash_features.h"
#include "base/feature_list.h"
#include "base/files/file_path.h"
#include "base/functional/bind.h"
#include "base/metrics/histogram_functions.h"
#include "base/no_destructor.h"
#include "base/strings/strcat.h"
#include "base/strings/stringprintf.h"
#include "base/task/single_thread_task_runner.h"
#include "base/time/time.h"
#include "chrome/browser/ash/crostini/crostini_features.h"
#include "chrome/browser/ash/crostini/crostini_manager.h"
#include "chrome/browser/ash/crostini/crostini_pref_names.h"
#include "chrome/browser/ash/file_manager/path_util.h"
#include "chrome/browser/ash/guest_os/guest_os_mime_types_service.h"
#include "chrome/browser/ash/guest_os/guest_os_mime_types_service_factory.h"
#include "chrome/browser/ash/guest_os/guest_os_pref_names.h"
#include "chrome/browser/ash/guest_os/guest_os_registry_service.h"
#include "chrome/browser/ash/guest_os/guest_os_registry_service_factory.h"
#include "chrome/browser/ash/guest_os/guest_os_session_tracker.h"
#include "chrome/browser/ash/guest_os/guest_os_share_path.h"
#include "chrome/browser/ash/guest_os/guest_os_terminal.h"
#include "chrome/browser/ash/guest_os/public/types.h"
#include "chrome/browser/ash/profiles/profile_helper.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/ash/shelf/app_service/app_service_app_window_crostini_tracker.h"
#include "chrome/browser/ui/ash/shelf/app_service/app_service_app_window_shelf_controller.h"
#include "chrome/browser/ui/ash/shelf/chrome_shelf_controller.h"
#include "chrome/browser/ui/ash/shelf/shelf_spinner_controller.h"
#include "chrome/browser/ui/ash/shelf/shelf_spinner_item_controller.h"
#include "chrome/browser/ui/views/crostini/crostini_recovery_view.h"
#include "chrome/browser/ui/webui/ash/crostini_upgrader/crostini_upgrader_dialog.h"
#include "chrome/common/chrome_features.h"
#include "chrome/grit/generated_resources.h"
#include "chromeos/ui/base/app_types.h"
#include "chromeos/ui/base/window_properties.h"
#include "components/prefs/pref_service.h"
#include "components/user_manager/user.h"
#include "ui/aura/client/aura_constants.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/l10n/time_format.h"

namespace crostini {
namespace {

constexpr char kCrostiniAppLaunchHistogram[] = "Crostini.AppLaunch";
constexpr char kCrostiniAppLaunchResultHistogram[] = "Crostini.AppLaunchResult";
constexpr char kCrostiniAppLaunchResultHistogramTerminal[] =
    "Crostini.AppLaunchResult.Terminal";
constexpr char kCrostiniAppLaunchResultHistogramRegistered[] =
    "Crostini.AppLaunchResult.Registered";
constexpr char kCrostiniAppLaunchResultHistogramUnknown[] =
    "Crostini.AppLaunchResult.Unknown";
constexpr int64_t kDelayBeforeSpinnerMs = 400;

void OnApplicationLaunched(const std::string& app_id,
                           crostini::CrostiniSuccessCallback callback,
                           const crostini::CrostiniResult failure_result,
                           bool success,
                           const std::string& failure_reason) {
  CrostiniAppLaunchAppType type = CrostiniAppLaunchAppType::kRegisteredApp;
  CrostiniResult result = success ? CrostiniResult::SUCCESS : failure_result;
  RecordAppLaunchResultHistogram(type, result);
  std::move(callback).Run(success, failure_reason);
}

void OnLaunchFailed(const std::string& app_id,
                    crostini::CrostiniSuccessCallback callback,
                    const std::string& failure_reason,
                    crostini::CrostiniResult result) {
  // Remove the spinner and icon. Controller doesn't exist in tests.
  // TODO(timloh): Consider also displaying a notification for failure.
  if (auto* chrome_controller = ChromeShelfController::instance()) {
    chrome_controller->GetShelfSpinnerController()->CloseSpinner(app_id);
  }
  OnApplicationLaunched(app_id, std::move(callback), result, false,
                        failure_reason);
}

void OnSharePathForLaunchApplication(
    Profile* profile,
    const std::string& app_id,
    guest_os::GuestOsRegistryService::Registration registration,
    const guest_os::GuestId& container_id,
    int64_t display_id,
    const std::vector<std::string>& args,
    crostini::CrostiniSuccessCallback callback,
    bool success,
    const std::string& failure_reason) {
  if (!success) {
    return OnLaunchFailed(
        app_id, std::move(callback),
        "Failed to share paths to launch " + app_id + ": " + failure_reason,
        CrostiniResult::SHARE_PATHS_FAILED);
  }

  guest_os::launcher::LaunchApplication(
      profile, container_id, std::move(registration), display_id, args,
      base::BindOnce(OnApplicationLaunched, app_id, std::move(callback),
                     crostini::CrostiniResult::UNKNOWN_ERROR));
}

void LaunchApplication(
    Profile* profile,
    const std::string& app_id,
    guest_os::GuestOsRegistryService::Registration registration,
    const guest_os::GuestId& container_id,
    int64_t display_id,
    const std::vector<guest_os::LaunchArg>& args,
    crostini::CrostiniSuccessCallback callback) {
  ChromeShelfController* chrome_shelf_controller =
      ChromeShelfController::instance();
  DCHECK(chrome_shelf_controller);

  AppServiceAppWindowShelfController* app_service_controller =
      chrome_shelf_controller->app_service_app_window_controller();
  DCHECK(app_service_controller);

  AppServiceAppWindowCrostiniTracker* crostini_tracker =
      app_service_controller->app_service_crostini_tracker();
  DCHECK(crostini_tracker);

  crostini_tracker->OnAppLaunchRequested(app_id, display_id);

  // Get vm_info because we need seneschal_server_handle.
  const std::string& vm_name = registration.VmName();
  auto vm_info =
      guest_os::GuestOsSessionTracker::GetForProfile(profile)->GetVmInfo(
          vm_name);
  if (!vm_info) {
    return OnLaunchFailed(app_id, std::move(callback),
                          "Crostini VM not running: " + vm_name,
                          CrostiniResult::SHARE_PATHS_FAILED);
  }

  // Share any paths not in crostini.  The user will see the spinner while this
  // is happening.
  auto* share_path = guest_os::GuestOsSharePath::GetForProfile(profile);
  auto paths_or_error = share_path->ConvertArgsToPathsToShare(
      registration, args, crostini::ContainerChromeOSBaseDirectory(),
      /*map_crostini_home=*/true);
  if (absl::holds_alternative<std::string>(paths_or_error)) {
    OnLaunchFailed(app_id, std::move(callback),
                   absl::get<std::string>(paths_or_error),
                   CrostiniResult::SHARE_PATHS_FAILED);
    return;
  }
  const auto& paths =
      absl::get<guest_os::GuestOsSharePath::PathsToShare>(paths_or_error);
  share_path->SharePaths(
      vm_name, vm_info->seneschal_server_handle(),
      std::move(paths.paths_to_share),
      base::BindOnce(OnSharePathForLaunchApplication, profile, app_id,
                     std::move(registration), container_id, display_id,
                     std::move(paths.launch_args), std::move(callback)));
}

}  // namespace

bool IsUninstallable(Profile* profile, const std::string& app_id) {
  if (!CrostiniFeatures::Get()->IsEnabled(profile)) {
    return false;
  }
  auto* registry_service =
      guest_os::GuestOsRegistryServiceFactory::GetForProfile(profile);
  std::optional<guest_os::GuestOsRegistryService::Registration> registration =
      registry_service->GetRegistration(app_id);
  if (registration) {
    return registration->CanUninstall();
  }
  return false;
}

bool IsCrostiniRunning(Profile* profile) {
  auto* manager = crostini::CrostiniManager::GetForProfile(profile);
  return manager && manager->IsVmRunning(kCrostiniDefaultVmName);
}

bool ShouldConfigureDefaultContainer(Profile* profile) {
  const base::FilePath ansible_playbook_file_path =
      profile->GetPrefs()->GetFilePath(prefs::kCrostiniAnsiblePlaybookFilePath);
  bool default_container_configured = profile->GetPrefs()->GetBoolean(
      prefs::kCrostiniDefaultContainerConfigured);
  return base::FeatureList::IsEnabled(
             features::kCrostiniAnsibleInfrastructure) &&
         !default_container_configured && !ansible_playbook_file_path.empty();
}

bool ShouldAllowContainerUpgrade(Profile* profile) {
  return CrostiniFeatures::Get()->IsContainerUpgradeUIAllowed(profile) &&
         crostini::CrostiniManager::GetForProfile(profile)
             ->IsContainerUpgradeable(DefaultContainerId());
}

void AddSpinner(crostini::CrostiniManager::RestartId restart_id,
                const std::string& app_id,
                Profile* profile) {
  ChromeShelfController* chrome_controller = ChromeShelfController::instance();
  if (chrome_controller &&
      crostini::CrostiniManager::GetForProfile(profile)->IsRestartPending(
          restart_id)) {
    chrome_controller->GetShelfSpinnerController()->AddSpinnerToShelf(
        app_id, std::make_unique<ShelfSpinnerItemController>(app_id));
  }
}

void LaunchCrostiniAppImpl(
    Profile* profile,
    const std::string& app_id,
    guest_os::GuestOsRegistryService::Registration registration,
    const guest_os::GuestId& container_id,
    int64_t display_id,
    const std::vector<guest_os::LaunchArg>& args,
    CrostiniSuccessCallback callback) {
  auto* crostini_manager = crostini::CrostiniManager::GetForProfile(profile);
  auto* registry_service =
      guest_os::GuestOsRegistryServiceFactory::GetForProfile(profile);
  RecordAppLaunchHistogram(CrostiniAppLaunchAppType::kRegisteredApp);

  // Update the last launched time and Termina version.
  registry_service->AppLaunched(app_id);
  crostini_manager->UpdateLaunchMetricsForEnterpriseReporting();

  auto spinner_app_id =
      registration.Terminal() ? guest_os::kTerminalSystemAppId : app_id;
  auto restart_id = crostini_manager->RestartCrostini(
      container_id,
      base::BindOnce(
          [](Profile* profile, const std::string& app_id,
             guest_os::GuestOsRegistryService::Registration registration,
             const guest_os::GuestId& container_id, int64_t display_id,
             const std::vector<guest_os::LaunchArg> args,
             crostini::CrostiniSuccessCallback callback,
             crostini::CrostiniResult result) {
            if (result != crostini::CrostiniResult::SUCCESS) {
              OnLaunchFailed(app_id, std::move(callback),
                             base::StringPrintf(
                                 "crostini restart to launch app %s failed: %d",
                                 app_id.c_str(), static_cast<int>(result)),
                             result);
              return;
            }

            LaunchApplication(profile, app_id, std::move(registration),
                              container_id, display_id, args,
                              std::move(callback));
          },
          profile, app_id, std::move(registration), container_id, display_id,
          args, std::move(callback)));

  base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
      FROM_HERE,
      base::BindOnce(&AddSpinner, restart_id, spinner_app_id, profile),
      base::Milliseconds(kDelayBeforeSpinnerMs));
}

void LaunchCrostiniAppWithIntent(Profile* profile,
                                 const std::string& app_id,
                                 int64_t display_id,
                                 apps::IntentPtr intent,
                                 const std::vector<guest_os::LaunchArg>& args,
                                 CrostiniSuccessCallback callback) {
  // Policies can change under us, and crostini may now be forbidden.
  std::string reason;
  if (!CrostiniFeatures::Get()->IsAllowedNow(profile, &reason)) {
    LOG(ERROR) << "Crostini not allowed: " << reason;
    return std::move(callback).Run(false, "Crostini UI not allowed");
  }

  auto* crostini_manager = crostini::CrostiniManager::GetForProfile(profile);
  auto* registry_service =
      guest_os::GuestOsRegistryServiceFactory::GetForProfile(profile);
  std::optional<guest_os::GuestOsRegistryService::Registration> registration =
      registry_service->GetRegistration(app_id);

  if (!registration) {
    RecordAppLaunchHistogram(CrostiniAppLaunchAppType::kUnknownApp);
    RecordAppLaunchResultHistogram(CrostiniAppLaunchAppType::kUnknownApp,
                                   CrostiniResult::UNREGISTERED_APPLICATION);
    return std::move(callback).Run(
        false, "LaunchCrostiniApp called with an unknown app_id: " + app_id);
  }
  guest_os::GuestId container_id(registration->VmType(), registration->VmName(),
                                 registration->ContainerName());

  if (crostini_manager->IsUncleanStartup()) {
    VLOG(1) << "Unclean startup for " << container_id
            << " - showing recovery view";
    // Prompt for user-restart.
    return ShowCrostiniRecoveryView(
        profile, crostini::CrostiniUISurface::kAppList, app_id, display_id,
        args, std::move(callback));
  }

  if (crostini_manager->GetCrostiniDialogStatus(DialogType::UPGRADER)) {
    // Reshow the existing dialog.
    ash::CrostiniUpgraderDialog::Reshow();
    VLOG(1) << "Reshowing upgrade dialog";
    std::move(callback).Run(
        false, "LaunchCrostiniApp called while upgrade dialog showing");
    return;
  }

  LaunchCrostiniAppImpl(profile, app_id, std::move(*registration), container_id,
                        display_id, args, std::move(callback));
}

void LaunchCrostiniApp(Profile* profile,
                       const std::string& app_id,
                       int64_t display_id,
                       const std::vector<guest_os::LaunchArg>& args,
                       CrostiniSuccessCallback callback) {
  LaunchCrostiniAppWithIntent(profile, app_id, display_id, nullptr, args,
                              std::move(callback));
}

std::vector<vm_tools::cicerone::ContainerFeature> GetContainerFeatures() {
  std::vector<vm_tools::cicerone::ContainerFeature> result;

  // TODO: b/303743348 - Update garcon to set this env var by default and
  // deprecate this feature.
  result.push_back(
      vm_tools::cicerone::ContainerFeature::ENABLE_GTK3_IME_SUPPORT);

  if (base::FeatureList::IsEnabled(ash::features::kCrostiniQtImeSupport)) {
    result.push_back(
        vm_tools::cicerone::ContainerFeature::ENABLE_QT_IME_SUPPORT);
  }
  if (base::FeatureList::IsEnabled(
          ash::features::kCrostiniVirtualKeyboardSupport)) {
    result.push_back(
        vm_tools::cicerone::ContainerFeature::ENABLE_VIRTUAL_KEYBOARD_SUPPORT);
  }
  return result;
}

std::string CryptohomeIdForProfile(Profile* profile) {
  std::string id = ash::ProfileHelper::GetUserIdHashFromProfile(profile);
  // Empty id means we're running in a test.
  return id.empty() ? "test" : id;
}

std::string DefaultContainerUserNameForProfile(Profile* profile) {
  const user_manager::User* user =
      ash::ProfileHelper::Get()->GetUserByProfile(profile);
  if (!user) {
    return kCrostiniDefaultUsername;
  }
  std::string username = user->GetAccountName(/*use_display_email=*/false);

  // For gmail accounts, dots are already stripped away in the canonical
  // username. But for other accounts (e.g. managedchrome), we need to do this
  // manually.
  std::string::size_type index;
  while ((index = username.find('.')) != std::string::npos) {
    username.erase(index, 1);
  }

  return username;
}

base::FilePath ContainerChromeOSBaseDirectory() {
  return base::FilePath("/mnt/chromeos");
}

void AddNewLxdContainerToPrefs(Profile* profile,
                               const guest_os::GuestId& container_id) {
  base::Value::Dict properties;
  properties.Set(guest_os::prefs::kContainerOsVersionKey,
                 static_cast<int>(ContainerOsVersion::kUnknown));
  properties.Set(guest_os::prefs::kContainerOsPrettyNameKey, "");
  guest_os::AddContainerToPrefs(profile, container_id, std::move(properties));
}

void RemoveLxdContainerFromPrefs(Profile* profile,
                                 const guest_os::GuestId& container_id) {
  guest_os::RemoveContainerFromPrefs(profile, container_id);
  guest_os::GuestOsRegistryServiceFactory::GetForProfile(profile)
      ->ClearApplicationList(guest_os::VmType::TERMINA, container_id.vm_name,
                             container_id.container_name);
  guest_os::GuestOsMimeTypesServiceFactory::GetForProfile(profile)
      ->ClearMimeTypes(container_id.vm_name, container_id.container_name);
}

SkColor GetContainerBadgeColor(Profile* profile,
                               const guest_os::GuestId& container_id) {
  const base::Value* badge_color_value = GetContainerPrefValue(
      profile, container_id, guest_os::prefs::kContainerColorKey);
  if (badge_color_value) {
    return badge_color_value->GetIfInt().value_or(SK_ColorTRANSPARENT);
  } else {
    return SK_ColorTRANSPARENT;
  }
}

void SetContainerBadgeColor(Profile* profile,
                            const guest_os::GuestId& container_id,
                            SkColor badge_color) {
  guest_os::UpdateContainerPref(profile, container_id,
                                guest_os::prefs::kContainerColorKey,
                                base::Value(static_cast<int>(badge_color)));

  guest_os::GuestOsRegistryServiceFactory::GetForProfile(profile)
      ->ContainerBadgeColorChanged(container_id);
}

bool IsContainerVersionExpired(Profile* profile,
                               const guest_os::GuestId& container_id) {
  auto* value = GetContainerPrefValue(profile, container_id,
                                      guest_os::prefs::kContainerOsVersionKey);
  if (!value) {
    return false;
  }

  auto version = static_cast<ContainerOsVersion>(value->GetInt());
  return version == ContainerOsVersion::kDebianStretch ||
         version == ContainerOsVersion::kDebianBuster;
}

std::u16string GetTimeRemainingMessage(base::TimeTicks start, int percent) {
  // Only estimate once we've spent at least 3 seconds OR gotten 10% of the way
  // through.
  constexpr base::TimeDelta kMinTimeForEstimate = base::Seconds(3);
  constexpr base::TimeDelta kTimeDeltaZero = base::Seconds(0);
  constexpr int kMinPercentForEstimate = 10;
  base::TimeDelta elapsed = base::TimeTicks::Now() - start;
  if ((elapsed >= kMinTimeForEstimate && percent > 0) ||
      (percent >= kMinPercentForEstimate && elapsed > kTimeDeltaZero)) {
    base::TimeDelta total_time_expected = (elapsed * 100) / percent;
    base::TimeDelta time_remaining = total_time_expected - elapsed;
    return ui::TimeFormat::Simple(ui::TimeFormat::FORMAT_REMAINING,
                                  ui::TimeFormat::LENGTH_SHORT, time_remaining);
  } else {
    return l10n_util::GetStringUTF16(
        IDS_CROSTINI_NOTIFICATION_OPERATION_STARTING);
  }
}

const guest_os::GuestId& DefaultContainerId() {
  static const base::NoDestructor<guest_os::GuestId> container_id(
      kCrostiniDefaultVmType, kCrostiniDefaultVmName,
      kCrostiniDefaultContainerName);
  return *container_id;
}

bool IsCrostiniWindow(const aura::Window* window) {
  // TODO(crbug/1158644): Non-Crostini apps (borealis, ...) have also been
  // identifying as Crostini. For now they're less common, and as they become
  // more productionised they get their own app type (e.g. lacros), but at some
  // point we'll want to untangle these different types to e.g. avoid double
  // counting in usage metrics.
  return window->GetProperty(chromeos::kAppTypeKey) ==
         chromeos::AppType::CROSTINI_APP;
}

void RecordAppLaunchHistogram(CrostiniAppLaunchAppType app_type) {
  base::UmaHistogramEnumeration(kCrostiniAppLaunchHistogram, app_type);
}

void RecordAppLaunchResultHistogram(CrostiniAppLaunchAppType type,
                                    crostini::CrostiniResult reason) {
  // We record one histogram for everything, so we have data continuity as
  // that's the metric we had first, and we also break results down by launch
  // type.
  base::UmaHistogramEnumeration(kCrostiniAppLaunchResultHistogram, reason);
  switch (type) {
    case CrostiniAppLaunchAppType::kTerminal:
      base::UmaHistogramEnumeration(kCrostiniAppLaunchResultHistogramTerminal,
                                    reason);
      break;
    case CrostiniAppLaunchAppType::kRegisteredApp:
      base::UmaHistogramEnumeration(kCrostiniAppLaunchResultHistogramRegistered,
                                    reason);
      break;
    case CrostiniAppLaunchAppType::kUnknownApp:
      base::UmaHistogramEnumeration(kCrostiniAppLaunchResultHistogramUnknown,
                                    reason);
      break;
  }
}

bool ShouldStopVm(Profile* profile, const guest_os::GuestId& container_id) {
  for (const auto& container :
       guest_os::GetContainers(profile, kCrostiniDefaultVmType)) {
    if (container.container_name != container_id.container_name &&
        container.vm_name == container_id.vm_name) {
      if (guest_os::GuestOsSessionTracker::GetForProfile(profile)->IsRunning(
              container)) {
        return false;
      }
    }
  }
  return true;
}

std::string FormatForUi(guest_os::GuestId guest_id) {
  if (guest_id.vm_name == kCrostiniDefaultVmName) {
    return guest_id.container_name;
  }
  return base::StrCat({guest_id.vm_name, ":", guest_id.container_name});
}

}  // namespace crostini