blob: 9437bbc782a7d0744425a4a11ea6b093ac8b693c [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/chromeos/crostini/crostini_util.h"
#include <utility>
#include "base/bind.h"
#include "base/callback.h"
#include "base/feature_list.h"
#include "base/files/file_path.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/strcat.h"
#include "base/strings/string_util.h"
#include "base/task/post_task.h"
#include "base/timer/timer.h"
#include "chrome/browser/chromeos/crostini/crostini_app_launch_observer.h"
#include "chrome/browser/chromeos/crostini/crostini_manager.h"
#include "chrome/browser/chromeos/crostini/crostini_mime_types_service.h"
#include "chrome/browser/chromeos/crostini/crostini_mime_types_service_factory.h"
#include "chrome/browser/chromeos/crostini/crostini_pref_names.h"
#include "chrome/browser/chromeos/crostini/crostini_registry_service.h"
#include "chrome/browser/chromeos/crostini/crostini_registry_service_factory.h"
#include "chrome/browser/chromeos/profiles/profile_helper.h"
#include "chrome/browser/chromeos/settings/cros_settings.h"
#include "chrome/browser/chromeos/virtual_machines/virtual_machines_util.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/app_list/crostini/crostini_app_icon.h"
#include "chrome/browser/ui/ash/launcher/chrome_launcher_controller.h"
#include "chrome/browser/ui/ash/launcher/shelf_spinner_controller.h"
#include "chrome/browser/ui/ash/launcher/shelf_spinner_item_controller.h"
#include "chrome/browser/ui/ash/window_properties.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/common/chrome_features.h"
#include "chrome/grit/chrome_unscaled_resources.h"
#include "chrome/grit/generated_resources.h"
#include "chromeos/constants/chromeos_features.h"
#include "components/prefs/pref_service.h"
#include "google_apis/gaia/gaia_auth_util.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/l10n/time_format.h"
namespace {
constexpr char kCrostiniAppLaunchHistogram[] = "Crostini.AppLaunch";
constexpr char kCrostiniAppNamePrefix[] = "_crostini_";
constexpr int64_t kDelayBeforeSpinnerMs = 400;
// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused.
enum class CrostiniAppLaunchAppType {
// An app which isn't in the CrostiniAppRegistry. This shouldn't happen.
kUnknownApp = 0,
// The main terminal app.
kTerminal = 1,
// An app for which there is something in the CrostiniAppRegistry.
kRegisteredApp = 2,
kCount
};
void RecordAppLaunchHistogram(CrostiniAppLaunchAppType app_type) {
base::UmaHistogramEnumeration(kCrostiniAppLaunchHistogram, app_type,
CrostiniAppLaunchAppType::kCount);
}
void OnLaunchFailed(const std::string& app_id) {
// Remove the spinner so it doesn't stay around forever.
// TODO(timloh): Consider also displaying a notification of some sort.
ChromeLauncherController* chrome_controller =
ChromeLauncherController::instance();
DCHECK(chrome_controller);
chrome_controller->GetShelfSpinnerController()->Close(app_id);
}
void OnCrostiniRestarted(Profile* profile,
const std::string& app_id,
Browser* browser,
base::OnceClosure callback,
crostini::CrostiniResult result) {
if (result != crostini::CrostiniResult::SUCCESS) {
OnLaunchFailed(app_id);
if (browser && browser->window())
browser->window()->Close();
if (result == crostini::CrostiniResult::OFFLINE_WHEN_UPGRADE_REQUIRED) {
ShowCrostiniUpgradeView(profile, crostini::CrostiniUISurface::kAppList);
}
return;
}
std::move(callback).Run();
}
void OnContainerApplicationLaunched(const std::string& app_id,
crostini::CrostiniResult result) {
if (result != crostini::CrostiniResult::SUCCESS)
OnLaunchFailed(app_id);
}
Browser* CreateTerminal(const AppLaunchParams& launch_params,
const GURL& vsh_in_crosh_url) {
return crostini::CrostiniManager::CreateContainerTerminal(launch_params,
vsh_in_crosh_url);
}
void ShowTerminal(const AppLaunchParams& launch_params,
const GURL& vsh_in_crosh_url,
Browser* browser) {
crostini::CrostiniManager::ShowContainerTerminal(launch_params,
vsh_in_crosh_url, browser);
browser->window()->GetNativeWindow()->SetProperty(
kOverrideWindowIconResourceIdKey, IDR_LOGO_CROSTINI_TERMINAL);
}
void LaunchContainerApplication(
Profile* profile,
const std::string& app_id,
crostini::CrostiniRegistryService::Registration registration,
int64_t display_id,
const std::vector<std::string>& files,
bool display_scaled) {
ChromeLauncherController* chrome_launcher_controller =
ChromeLauncherController::instance();
DCHECK_NE(chrome_launcher_controller, nullptr);
CrostiniAppLaunchObserver* observer =
chrome_launcher_controller->crostini_app_window_shelf_controller();
DCHECK_NE(observer, nullptr);
observer->OnAppLaunchRequested(app_id, display_id);
crostini::CrostiniManager::GetForProfile(profile)->LaunchContainerApplication(
registration.VmName(), registration.ContainerName(),
registration.DesktopFileId(), files, display_scaled,
base::BindOnce(OnContainerApplicationLaunched, app_id));
}
// Helper class for loading icons. The callback is called when all icons have
// been loaded, or after a provided timeout, after which the object deletes
// itself.
// TODO(timloh): We should consider having a service, so multiple requests for
// the same icon won't load the same image multiple times and only the first
// request would incur the loading delay.
class IconLoadWaiter : public CrostiniAppIcon::Observer {
public:
static void LoadIcons(
Profile* profile,
const std::vector<std::string>& app_ids,
int resource_size_in_dip,
ui::ScaleFactor scale_factor,
base::TimeDelta timeout,
base::OnceCallback<void(const std::vector<gfx::ImageSkia>&)> callback) {
new IconLoadWaiter(profile, app_ids, resource_size_in_dip, scale_factor,
timeout, std::move(callback));
}
private:
IconLoadWaiter(
Profile* profile,
const std::vector<std::string>& app_ids,
int resource_size_in_dip,
ui::ScaleFactor scale_factor,
base::TimeDelta timeout,
base::OnceCallback<void(const std::vector<gfx::ImageSkia>&)> callback)
: callback_(std::move(callback)) {
for (const std::string& app_id : app_ids) {
icons_.push_back(std::make_unique<CrostiniAppIcon>(
profile, app_id, resource_size_in_dip, this));
icons_.back()->LoadForScaleFactor(scale_factor);
}
timeout_timer_.Start(FROM_HERE, timeout, this,
&IconLoadWaiter::RunCallback);
}
// TODO(timloh): This is only called when an icon is found, so if any of the
// requested apps are missing an icon, we'll have to wait for the timeout. We
// should add an interface so we can avoid this.
void OnIconUpdated(CrostiniAppIcon* icon) override {
loaded_icons_++;
if (loaded_icons_ != icons_.size())
return;
timeout_timer_.AbandonAndStop();
RunCallback();
}
void Delete() {
DCHECK(!timeout_timer_.IsRunning());
delete this;
}
void RunCallback() {
DCHECK(callback_);
std::vector<gfx::ImageSkia> result;
for (const auto& icon : icons_)
result.emplace_back(icon->image_skia());
std::move(callback_).Run(result);
// If we're running the callback as loading has finished, we can't delete
// ourselves yet as it would destroy the CrostiniAppIcon which is calling
// into us right now.
base::SequencedTaskRunnerHandle::Get()->PostTask(
FROM_HERE,
base::BindOnce(&IconLoadWaiter::Delete, base::Unretained(this)));
}
std::vector<std::unique_ptr<CrostiniAppIcon>> icons_;
size_t loaded_icons_ = 0;
base::OneShotTimer timeout_timer_;
base::OnceCallback<void(const std::vector<gfx::ImageSkia>&)> callback_;
};
bool IsCrostiniAllowedForProfileImpl(Profile* profile) {
if (!profile || profile->IsChild() || profile->IsLegacySupervised() ||
profile->IsOffTheRecord() ||
chromeos::ProfileHelper::IsEphemeralUserProfile(profile) ||
chromeos::ProfileHelper::IsLockScreenAppProfile(profile)) {
return false;
}
if (!crostini::CrostiniManager::IsDevKvmPresent()) {
// Hardware is physically incapable, no matter what the user wants.
return false;
}
return virtual_machines::AreVirtualMachinesAllowedByVersionAndChannel() &&
base::FeatureList::IsEnabled(features::kCrostini);
}
} // namespace
namespace crostini {
std::string ContainerIdToString(const ContainerId& container_id) {
return base::StrCat(
{"(", container_id.first, ", ", container_id.second, ")"});
}
bool IsCrostiniAllowedForProfile(Profile* profile) {
const user_manager::User* user =
chromeos::ProfileHelper::Get()->GetUserByProfile(profile);
if (!IsUnaffiliatedCrostiniAllowedByPolicy() && !user->IsAffiliated()) {
return false;
}
if (!profile->GetPrefs()->GetBoolean(
crostini::prefs::kUserCrostiniAllowedByPolicy)) {
return false;
}
if (!virtual_machines::AreVirtualMachinesAllowedByPolicy()) {
return false;
}
return IsCrostiniAllowedForProfileImpl(profile);
}
bool IsCrostiniUIAllowedForProfile(Profile* profile, bool check_policy) {
if (!chromeos::ProfileHelper::IsPrimaryProfile(profile)) {
return false;
}
if (check_policy) {
return IsCrostiniAllowedForProfile(profile);
}
return IsCrostiniAllowedForProfileImpl(profile);
}
bool IsCrostiniExportImportUIAllowedForProfile(Profile* profile) {
return IsCrostiniUIAllowedForProfile(profile, true) &&
base::FeatureList::IsEnabled(chromeos::features::kCrostiniBackup) &&
profile->GetPrefs()->GetBoolean(
crostini::prefs::kUserCrostiniExportImportUIAllowedByPolicy);
}
bool IsCrostiniEnabled(Profile* profile) {
return IsCrostiniUIAllowedForProfile(profile) &&
profile->GetPrefs()->GetBoolean(crostini::prefs::kCrostiniEnabled);
}
bool IsCrostiniRunning(Profile* profile) {
return crostini::CrostiniManager::GetForProfile(profile)->IsVmRunning(
kCrostiniDefaultVmName);
}
void LaunchCrostiniApp(Profile* profile,
const std::string& app_id,
int64_t display_id) {
LaunchCrostiniApp(profile, app_id, display_id, std::vector<std::string>());
}
void AddSpinner(crostini::CrostiniManager::RestartId restart_id,
const std::string& app_id,
Profile* profile,
std::string vm_name,
std::string container_name) {
ChromeLauncherController* chrome_controller =
ChromeLauncherController::instance();
if (chrome_controller &&
crostini::CrostiniManager::GetForProfile(profile)->IsRestartPending(
restart_id)) {
chrome_controller->GetShelfSpinnerController()->AddSpinnerToShelf(
app_id, std::make_unique<ShelfSpinnerItemController>(app_id));
}
}
void LaunchCrostiniApp(Profile* profile,
const std::string& app_id,
int64_t display_id,
const std::vector<std::string>& files) {
// Policies can change under us, and crostini may now be forbidden.
if (!IsCrostiniUIAllowedForProfile(profile)) {
return;
}
auto* crostini_manager = crostini::CrostiniManager::GetForProfile(profile);
crostini::CrostiniRegistryService* registry_service =
crostini::CrostiniRegistryServiceFactory::GetForProfile(profile);
base::Optional<crostini::CrostiniRegistryService::Registration> registration =
registry_service->GetRegistration(app_id);
if (!registration) {
RecordAppLaunchHistogram(CrostiniAppLaunchAppType::kUnknownApp);
LOG(ERROR) << "LaunchCrostiniApp called with an unknown app_id: " << app_id;
return;
}
// Store these as we move |registration| into LaunchContainerApplication().
const std::string vm_name = registration->VmName();
const std::string container_name = registration->ContainerName();
base::OnceClosure launch_closure;
Browser* browser = nullptr;
if (app_id == kCrostiniTerminalId) {
DCHECK(files.empty());
RecordAppLaunchHistogram(CrostiniAppLaunchAppType::kTerminal);
// At this point, we know that Crostini UI is allowed.
if (!crostini_manager->IsCrosTerminaInstalled() ||
!profile->GetPrefs()->GetBoolean(crostini::prefs::kCrostiniEnabled)) {
ShowCrostiniInstallerView(profile, CrostiniUISurface::kAppList);
return;
}
GURL vsh_in_crosh_url = crostini::CrostiniManager::GenerateVshInCroshUrl(
profile, vm_name, container_name, std::vector<std::string>());
AppLaunchParams launch_params =
crostini::CrostiniManager::GenerateTerminalAppLaunchParams(profile);
// Create the terminal here so it's created in the right display. If the
// browser creation is delayed into the callback the root window for new
// windows setting can be changed due to the launcher or shelf dismissal.
Browser* browser = CreateTerminal(launch_params, vsh_in_crosh_url);
launch_closure =
base::BindOnce(&ShowTerminal, launch_params, vsh_in_crosh_url, browser);
} else {
RecordAppLaunchHistogram(CrostiniAppLaunchAppType::kRegisteredApp);
launch_closure = base::BindOnce(
&LaunchContainerApplication, profile, app_id, std::move(*registration),
display_id, std::move(files), registration->IsScaled());
}
// Update the last launched time and Termina version.
registry_service->AppLaunched(app_id);
crostini_manager->UpdateLaunchMetricsForEnterpriseReporting();
auto restart_id = crostini_manager->RestartCrostini(
vm_name, container_name,
base::BindOnce(OnCrostiniRestarted, profile, app_id, browser,
std::move(launch_closure)));
base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
FROM_HERE,
base::BindOnce(&AddSpinner, restart_id, app_id, profile, vm_name,
container_name),
base::TimeDelta::FromMilliseconds(kDelayBeforeSpinnerMs));
}
void LoadIcons(Profile* profile,
const std::vector<std::string>& app_ids,
int resource_size_in_dip,
ui::ScaleFactor scale_factor,
base::TimeDelta timeout,
base::OnceCallback<void(const std::vector<gfx::ImageSkia>&)>
icons_loaded_callback) {
IconLoadWaiter::LoadIcons(profile, app_ids, resource_size_in_dip,
scale_factor, timeout,
std::move(icons_loaded_callback));
}
std::string CryptohomeIdForProfile(Profile* profile) {
std::string id = chromeos::ProfileHelper::GetUserIdHashFromProfile(profile);
// Empty id means we're running in a test.
return id.empty() ? "test" : id;
}
std::string DefaultContainerUserNameForProfile(Profile* profile) {
// Get rid of the @domain.name in the profile user name (an email address).
std::string container_username = profile->GetProfileUserName();
if (container_username.find('@') != std::string::npos) {
// gaia::CanonicalizeEmail CHECKs its argument contains'@'.
container_username = gaia::CanonicalizeEmail(container_username);
// |container_username| may have changed, so we have to find again.
return container_username.substr(0, container_username.find('@'));
}
return container_username;
}
base::FilePath ContainerChromeOSBaseDirectory() {
return base::FilePath("/mnt/chromeos");
}
std::string AppNameFromCrostiniAppId(const std::string& id) {
return kCrostiniAppNamePrefix + id;
}
base::Optional<std::string> CrostiniAppIdFromAppName(
const std::string& app_name) {
if (!base::StartsWith(app_name, kCrostiniAppNamePrefix,
base::CompareCase::SENSITIVE)) {
return base::nullopt;
}
return app_name.substr(strlen(kCrostiniAppNamePrefix));
}
bool IsUnaffiliatedCrostiniAllowedByPolicy() {
bool unaffiliated_crostini_allowed;
if (chromeos::CrosSettings::Get()->GetBoolean(
chromeos::kDeviceUnaffiliatedCrostiniAllowed,
&unaffiliated_crostini_allowed)) {
return unaffiliated_crostini_allowed;
}
// If device policy is not set, allow Crostini.
return true;
}
void AddNewLxdContainerToPrefs(Profile* profile,
std::string vm_name,
std::string container_name) {
auto* pref_service = profile->GetPrefs();
base::Value new_container(base::Value::Type::DICTIONARY);
new_container.SetKey(prefs::kVmKey, base::Value(vm_name));
new_container.SetKey(prefs::kContainerKey, base::Value(container_name));
ListPrefUpdate updater(pref_service, crostini::prefs::kCrostiniContainers);
updater->GetList().emplace_back(std::move(new_container));
}
void RemoveLxdContainerFromPrefs(Profile* profile,
std::string vm_name,
std::string container_name) {
auto* pref_service = profile->GetPrefs();
ListPrefUpdate updater(pref_service, crostini::prefs::kCrostiniContainers);
for (auto it = updater->GetList().begin(); it != updater->GetList().end();
it++) {
auto* vm_name_test = it->FindKey(prefs::kVmKey);
auto* container_name_test = it->FindKey(prefs::kContainerKey);
if (vm_name_test->GetString() == vm_name &&
container_name_test->GetString() == container_name) {
updater->GetList().erase(it);
break;
}
}
CrostiniRegistryServiceFactory::GetForProfile(profile)->ClearApplicationList(
vm_name, container_name);
CrostiniMimeTypesServiceFactory::GetForProfile(profile)->ClearMimeTypes(
vm_name, container_name);
}
base::string16 GetTimeRemainingMessage(base::Time 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::TimeDelta::FromSeconds(3);
constexpr base::TimeDelta kTimeDeltaZero = base::TimeDelta::FromSeconds(0);
constexpr int kMinPercentForEstimate = 10;
base::TimeDelta elapsed = base::Time::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);
}
}
} // namespace crostini