blob: f8f09f140e7c094bdbde9c4f22bf068e0e39f954 [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/login/demo_mode/demo_session.h"
#include <algorithm>
#include <utility>
#include "base/bind.h"
#include "base/callback.h"
#include "base/command_line.h"
#include "base/files/file_util.h"
#include "base/optional.h"
#include "base/path_service.h"
#include "base/stl_util.h"
#include "base/sys_info.h"
#include "base/task/post_task.h"
#include "chrome/browser/apps/platform_apps/app_load_service.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/browser_process_platform_part.h"
#include "chrome/browser/chromeos/file_manager/path_util.h"
#include "chrome/browser/chromeos/login/demo_mode/demo_setup_controller.h"
#include "chrome/browser/chromeos/policy/browser_policy_connector_chromeos.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/ui/extensions/app_launch_params.h"
#include "chrome/browser/ui/extensions/application_launch.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/common/extensions/extension_constants.h"
#include "chrome/common/pref_names.h"
#include "chromeos/chromeos_paths.h"
#include "chromeos/dbus/dbus_thread_manager.h"
#include "chromeos/dbus/image_loader_client.h"
#include "chromeos/settings/install_attributes.h"
#include "components/prefs/pref_service.h"
#include "components/session_manager/core/session_manager.h"
#include "components/user_manager/user.h"
#include "components/user_manager/user_manager.h"
#include "content/public/browser/browser_thread.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/common/constants.h"
#include "net/base/network_change_notifier.h"
namespace chromeos {
namespace {
// Global DemoSession instance.
DemoSession* g_demo_session = nullptr;
// Type of demo config forced on for tests.
base::Optional<DemoSession::DemoModeConfig> g_force_demo_config;
// Path relative to the path at which offline demo resources are loaded that
// contains image with demo Android apps.
constexpr base::FilePath::CharType kDemoAppsPath[] =
FILE_PATH_LITERAL("android_demo_apps.squash");
constexpr base::FilePath::CharType kExternalExtensionsPrefsPath[] =
FILE_PATH_LITERAL("demo_extensions.json");
// Path relative to the path at which offline demo resources are loaded that
// contains the highlights app.
constexpr char kHighlightsAppPath[] = "chrome_apps/highlights";
// Path relative to the path at which offline demo resources are loaded that
// contains sample photos.
constexpr char kPhotosPath[] = "media/photos";
bool IsDemoModeOfflineEnrolled() {
DCHECK(DemoSession::IsDeviceInDemoMode());
return DemoSession::GetDemoConfig() == DemoSession::DemoModeConfig::kOffline;
}
// Returns the list of apps normally pinned by Demo Mode policy that shouldn't
// be pinned if the device is offline.
std::vector<std::string> GetIgnorePinPolicyApps() {
return {
// Popular third-party game preinstalled in Demo Mode that is
// online-only, so shouldn't be featured in the shelf when offline.
"com.pixonic.wwr.chbkdemo",
// TODO(michaelpg): YouTube is also pinned as a *default* app.
extension_misc::kYoutubeAppId,
};
}
// Copies photos into the Downloads directory.
// TODO(michaelpg): Test this behavior (requires overriding the Downloads
// directory).
void InstallDemoMedia(base::FilePath offline_resources_path) {
if (offline_resources_path.empty()) {
LOG(ERROR) << "Offline resources not loaded - no media available.";
return;
}
base::FilePath src_path = offline_resources_path.Append(kPhotosPath);
base::FilePath dest_path = file_manager::util::GetDownloadsFolderForProfile(
ProfileManager::GetActiveUserProfile());
if (!base::CopyDirectory(src_path, dest_path, false /* recursive */))
LOG(ERROR) << "Failed to install demo mode media.";
}
std::string GetBoardName() {
const std::vector<std::string> board =
base::SplitString(base::SysInfo::GetLsbReleaseBoard(), "-",
base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
return board[0];
}
std::string GetHighlightsAppId() {
if (GetBoardName() == "eve")
return extension_misc::kHighlightsAlt1AppId;
if (GetBoardName() == "nocturne")
return extension_misc::kHighlightsAlt2AppId;
return extension_misc::kHighlightsAppId;
}
} // namespace
// static
const char DemoSession::kDemoModeResourcesComponentName[] =
"demo-mode-resources";
// static
base::FilePath DemoSession::GetPreInstalledDemoResourcesPath() {
base::FilePath preinstalled_components_root;
base::PathService::Get(DIR_PREINSTALLED_COMPONENTS,
&preinstalled_components_root);
return preinstalled_components_root.AppendASCII("cros-components")
.AppendASCII(kDemoModeResourcesComponentName);
}
// static
std::string DemoSession::DemoConfigToString(
DemoSession::DemoModeConfig config) {
switch (config) {
case DemoSession::DemoModeConfig::kNone:
return "none";
case DemoSession::DemoModeConfig::kOnline:
return "online";
case DemoSession::DemoModeConfig::kOffline:
return "offline";
}
NOTREACHED() << "Unknown demo mode configuration";
return std::string();
}
// static
bool DemoSession::IsDeviceInDemoMode() {
return GetDemoConfig() != DemoModeConfig::kNone;
}
// static
DemoSession::DemoModeConfig DemoSession::GetDemoConfig() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (g_force_demo_config.has_value())
return *g_force_demo_config;
const policy::BrowserPolicyConnectorChromeOS* const connector =
g_browser_process->platform_part()->browser_policy_connector_chromeos();
bool is_demo_device_mode = connector->GetInstallAttributes()->GetMode() ==
policy::DeviceMode::DEVICE_MODE_DEMO;
bool is_demo_device_domain = connector->GetInstallAttributes()->GetDomain() ==
DemoSetupController::kDemoModeDomain;
// TODO(agawronska): We check device mode and domain to allow for dev/test
// setup that is done by manual enrollment into demo domain. Device mode is
// not set to DeviceMode::DEVICE_MODE_DEMO then. This extra condition
// can be removed when all following conditions are fulfilled:
// * DMServer is returning DeviceMode::DEVICE_MODE_DEMO for demo devices
// * Offline policies specify DeviceMode::DEVICE_MODE_DEMO
// * Demo mode setup flow is available to external developers
bool is_demo_mode = is_demo_device_mode || is_demo_device_domain;
const PrefService* prefs = g_browser_process->local_state();
// The testing browser process might not have local state.
if (!prefs)
return DemoModeConfig::kNone;
// Demo mode config preference is set at the end of the demo setup after
// device is enrolled.
auto demo_config = DemoModeConfig::kNone;
int demo_config_pref = prefs->GetInteger(prefs::kDemoModeConfig);
if (demo_config_pref >= static_cast<int>(DemoModeConfig::kNone) &&
demo_config_pref <= static_cast<int>(DemoModeConfig::kLast)) {
demo_config = static_cast<DemoModeConfig>(demo_config_pref);
}
if (is_demo_mode && demo_config == DemoModeConfig::kNone) {
LOG(WARNING) << "Device mode is demo, but no demo mode config set";
} else if (!is_demo_mode && demo_config != DemoModeConfig::kNone) {
LOG(WARNING) << "Device mode is not demo, but demo mode config is set";
}
return is_demo_mode ? demo_config : DemoModeConfig::kNone;
}
// static
void DemoSession::SetDemoConfigForTesting(DemoModeConfig demo_config) {
g_force_demo_config = demo_config;
}
// static
void DemoSession::ResetDemoConfigForTesting() {
g_force_demo_config = base::nullopt;
}
// static
void DemoSession::PreloadOfflineResourcesIfInDemoMode() {
if (!IsDeviceInDemoMode())
return;
if (!g_demo_session)
g_demo_session = new DemoSession();
g_demo_session->EnsureOfflineResourcesLoaded(base::OnceClosure());
}
// static
DemoSession* DemoSession::StartIfInDemoMode() {
if (!IsDeviceInDemoMode())
return nullptr;
if (g_demo_session && g_demo_session->started())
return g_demo_session;
if (!g_demo_session)
g_demo_session = new DemoSession();
g_demo_session->started_ = true;
g_demo_session->EnsureOfflineResourcesLoaded(base::OnceClosure());
return g_demo_session;
}
// static
void DemoSession::ShutDownIfInitialized() {
if (!g_demo_session)
return;
DemoSession* demo_session = g_demo_session;
g_demo_session = nullptr;
delete demo_session;
}
// static
DemoSession* DemoSession::Get() {
return g_demo_session;
}
// static
std::string DemoSession::GetScreensaverAppId() {
if (GetBoardName() == "eve")
return extension_misc::kScreensaverAlt1AppId;
if (GetBoardName() == "nocturne")
return extension_misc::kScreensaverAlt2AppId;
return extension_misc::kScreensaverAppId;
}
// static
bool DemoSession::ShouldDisplayInAppLauncher(const std::string& app_id) {
if (!IsDeviceInDemoMode())
return true;
return app_id != GetScreensaverAppId() &&
app_id != extensions::kWebStoreAppId &&
app_id != extension_misc::kGeniusAppId;
}
void DemoSession::EnsureOfflineResourcesLoaded(
base::OnceClosure load_callback) {
if (offline_resources_loaded_) {
if (load_callback)
std::move(load_callback).Run();
return;
}
if (load_callback)
offline_resources_load_callbacks_.emplace_back(std::move(load_callback));
if (offline_resources_load_requested_)
return;
offline_resources_load_requested_ = true;
if (offline_enrolled_) {
LoadPreinstalledOfflineResources();
return;
}
component_updater::CrOSComponentManager* cros_component_manager =
g_browser_process->platform_part()->cros_component_manager();
if (cros_component_manager) {
g_browser_process->platform_part()->cros_component_manager()->Load(
kDemoModeResourcesComponentName,
component_updater::CrOSComponentManager::MountPolicy::kMount,
component_updater::CrOSComponentManager::UpdatePolicy::kSkip,
base::BindOnce(&DemoSession::InstalledComponentLoaded,
weak_ptr_factory_.GetWeakPtr()));
} else {
// Cros component manager may be unset in tests - if that is the case,
// report component install failure, so DemoSession attempts loading the
// component directly from the pre-installed component path.
// TODO(michaelpg): Rework tests to require the online component to load in
// online-enrolled demo mode.
InstalledComponentLoaded(
component_updater::CrOSComponentManager::Error::INSTALL_FAILURE,
base::FilePath());
}
}
void DemoSession::SetOfflineResourcesLoadedForTesting(
const base::FilePath& path) {
OnOfflineResourcesLoaded(path);
}
base::FilePath DemoSession::GetDemoAppsPath() const {
if (offline_resources_path_.empty())
return base::FilePath();
return offline_resources_path_.Append(kDemoAppsPath);
}
base::FilePath DemoSession::GetExternalExtensionsPrefsPath() const {
if (offline_resources_path_.empty())
return base::FilePath();
return offline_resources_path_.Append(kExternalExtensionsPrefsPath);
}
base::FilePath DemoSession::GetOfflineResourceAbsolutePath(
const base::FilePath& relative_path) const {
if (offline_resources_path_.empty())
return base::FilePath();
if (relative_path.ReferencesParent())
return base::FilePath();
return offline_resources_path_.Append(relative_path);
}
bool DemoSession::ShouldIgnorePinPolicy(const std::string& app_id_or_package) {
if (!g_demo_session || !g_demo_session->started())
return false;
// TODO(michaelpg): Update shelf when network status changes.
// TODO(michaelpg): Also check for captive portal.
if (!net::NetworkChangeNotifier::IsOffline())
return false;
return base::ContainsValue(ignore_pin_policy_offline_apps_,
app_id_or_package);
}
void DemoSession::SetExtensionsExternalLoader(
scoped_refptr<DemoExtensionsExternalLoader> extensions_external_loader) {
extensions_external_loader_ = extensions_external_loader;
InstallAppFromUpdateUrl(GetScreensaverAppId());
}
void DemoSession::OverrideIgnorePinPolicyAppsForTesting(
std::vector<std::string> apps) {
ignore_pin_policy_offline_apps_ = std::move(apps);
}
DemoSession::DemoSession()
: offline_enrolled_(IsDemoModeOfflineEnrolled()),
ignore_pin_policy_offline_apps_(GetIgnorePinPolicyApps()),
session_manager_observer_(this),
extension_registry_observer_(this),
weak_ptr_factory_(this) {
session_manager_observer_.Add(session_manager::SessionManager::Get());
OnSessionStateChanged();
}
DemoSession::~DemoSession() = default;
void DemoSession::InstalledComponentLoaded(
component_updater::CrOSComponentManager::Error error,
const base::FilePath& path) {
if (error == component_updater::CrOSComponentManager::Error::NONE) {
OnOfflineResourcesLoaded(base::make_optional(path));
return;
}
LoadPreinstalledOfflineResources();
}
void DemoSession::LoadPreinstalledOfflineResources() {
chromeos::DBusThreadManager::Get()
->GetImageLoaderClient()
->LoadComponentAtPath(
kDemoModeResourcesComponentName, GetPreInstalledDemoResourcesPath(),
base::BindOnce(&DemoSession::OnOfflineResourcesLoaded,
weak_ptr_factory_.GetWeakPtr()));
}
void DemoSession::OnOfflineResourcesLoaded(
base::Optional<base::FilePath> mounted_path) {
offline_resources_loaded_ = true;
if (mounted_path.has_value())
offline_resources_path_ = mounted_path.value();
std::list<base::OnceClosure> load_callbacks;
load_callbacks.swap(offline_resources_load_callbacks_);
for (auto& callback : load_callbacks)
std::move(callback).Run();
}
void DemoSession::InstallDemoResources() {
DCHECK(offline_resources_loaded_);
if (offline_enrolled_)
LoadAndLaunchHighlightsApp();
base::PostTaskWithTraits(
FROM_HERE, {base::TaskPriority::USER_VISIBLE, base::MayBlock()},
base::BindOnce(&InstallDemoMedia, offline_resources_path_));
}
void DemoSession::LoadAndLaunchHighlightsApp() {
DCHECK(offline_resources_loaded_);
if (offline_resources_path_.empty()) {
LOG(ERROR) << "Offline resources not loaded - no highlights app available.";
InstallAppFromUpdateUrl(GetHighlightsAppId());
return;
}
Profile* profile = ProfileManager::GetActiveUserProfile();
DCHECK(profile);
const base::FilePath resources_path =
offline_resources_path_.Append(kHighlightsAppPath);
if (!apps::AppLoadService::Get(profile)->LoadAndLaunch(
resources_path, base::CommandLine(base::CommandLine::NO_PROGRAM),
base::FilePath() /* cur_dir */)) {
LOG(WARNING) << "Failed to launch highlights app from offline resources.";
InstallAppFromUpdateUrl(GetHighlightsAppId());
}
}
void DemoSession::InstallAppFromUpdateUrl(const std::string& id) {
if (!extensions_external_loader_)
return;
auto* user = user_manager::UserManager::Get()->GetActiveUser();
if (!user->is_profile_created()) {
user->AddProfileCreatedObserver(
base::BindOnce(&DemoSession::InstallAppFromUpdateUrl,
weak_ptr_factory_.GetWeakPtr(), id));
return;
}
Profile* profile = ProfileManager::GetActiveUserProfile();
DCHECK(profile);
extension_registry_observer_.Add(extensions::ExtensionRegistry::Get(profile));
extensions_external_loader_->LoadApp(id);
}
void DemoSession::OnSessionStateChanged() {
if (session_manager::SessionManager::Get()->session_state() !=
session_manager::SessionState::ACTIVE) {
return;
}
if (!offline_enrolled_)
InstallAppFromUpdateUrl(GetHighlightsAppId());
EnsureOfflineResourcesLoaded(base::BindOnce(
&DemoSession::InstallDemoResources, weak_ptr_factory_.GetWeakPtr()));
}
void DemoSession::OnExtensionInstalled(content::BrowserContext* browser_context,
const extensions::Extension* extension,
bool is_update) {
if (extension->id() != GetHighlightsAppId())
return;
Profile* profile = ProfileManager::GetActiveUserProfile();
DCHECK(profile);
OpenApplication(AppLaunchParams(
profile, extension, extensions::LAUNCH_CONTAINER_WINDOW,
WindowOpenDisposition::NEW_WINDOW, extensions::SOURCE_CHROME_INTERNAL));
}
} // namespace chromeos