blob: 53ed89b23bcecb21ce0a734184bea908a034c457 [file] [log] [blame]
// Copyright 2017 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/ash/lock_screen_apps/app_manager_impl.h"
#include <memory>
#include <string>
#include <utility>
#include "apps/launcher.h"
#include "base/bind.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/memory/ref_counted.h"
#include "base/metrics/histogram_macros.h"
#include "base/sequenced_task_runner.h"
#include "base/threading/thread_task_runner_handle.h"
#include "base/time/tick_clock.h"
#include "chrome/browser/ash/lock_screen_apps/lock_screen_profile_creator.h"
#include "chrome/browser/ash/profiles/profile_helper.h"
#include "chrome/browser/chromeos/note_taking_helper.h"
#include "chrome/browser/extensions/extension_assets_manager.h"
#include "chrome/browser/extensions/extension_management.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/common/pref_names.h"
#include "extensions/browser/extension_file_task_runner.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/extension_system.h"
#include "extensions/browser/install_flag.h"
#include "extensions/common/api/app_runtime.h"
#include "extensions/common/extension.h"
#include "extensions/common/extension_urls.h"
#include "extensions/common/file_util.h"
#include "extensions/common/manifest.h"
namespace lock_screen_apps {
namespace {
using ExtensionCallback = base::OnceCallback<void(
const scoped_refptr<const extensions::Extension>& extension)>;
// The max number of times the lock screen app can be relaoded if it gets
// terminated while the lock screen is active.
constexpr int kMaxLockScreenAppReloadsCount = 3;
// The lock screen note taking availability state.
// Used to report UMA histograms - the values should map to
// LockScreenActionAvailability UMA enum values, and the values assigned to
// enum states should NOT be changed.
enum class ActionAvailability {
kAvailable = 0,
kNoActionHandlerApp = 1,
kAppNotSupportingLockScreen = 2,
kActionNotEnabledOnLockScreen = 3,
kDisallowedByPolicy = 4,
kLockScreenProfileNotCreated = 5,
kCount,
};
// The reason the note taking app was unloaded from the lock screen apps
// profile.
// Used to report UMA histograms - the values should map to
// LockScreenAppUnloadStatus UMA enum values, and the values assigned to
// enum states should NOT be changed.
enum class AppUnloadStatus {
kNotTerminated = 0,
kTerminatedReloadable = 1,
kTerminatedReloadAttemptsExceeded = 2,
kCount = 3
};
ActionAvailability GetLockScreenNoteTakingAvailability(
chromeos::NoteTakingAppInfo* app_info) {
if (!app_info || !app_info->preferred)
return ActionAvailability::kNoActionHandlerApp;
switch (app_info->lock_screen_support) {
case chromeos::NoteTakingLockScreenSupport::kNotSupported:
return ActionAvailability::kAppNotSupportingLockScreen;
case chromeos::NoteTakingLockScreenSupport::kSupported:
return ActionAvailability::kActionNotEnabledOnLockScreen;
case chromeos::NoteTakingLockScreenSupport::kNotAllowedByPolicy:
return ActionAvailability::kDisallowedByPolicy;
case chromeos::NoteTakingLockScreenSupport::kEnabled:
return ActionAvailability::kAvailable;
}
return ActionAvailability::kAppNotSupportingLockScreen;
}
void InvokeCallbackOnTaskRunner(
ExtensionCallback callback,
const scoped_refptr<base::SequencedTaskRunner>& task_runner,
const scoped_refptr<const extensions::Extension>& extension) {
task_runner->PostTask(FROM_HERE,
base::BindOnce(std::move(callback), extension));
}
// Loads extension with the provided |extension_id|, |location|, and
// |creation_flags| from the |version_dir| directory - directory to which the
// extension has been installed.
// |temp_copy| - scoped dir that contains the path from which extension
// resources have been installed. Not used in this method, but passed around
// to keep the directory in scope while the app is being installed.
// |callback| - callback to which the loaded app should be passed.
void LoadInstalledExtension(const std::string& extension_id,
extensions::mojom::ManifestLocation install_source,
int creation_flags,
std::unique_ptr<base::ScopedTempDir> temp_copy,
ExtensionCallback callback,
const base::FilePath& version_dir) {
if (version_dir.empty()) {
std::move(callback).Run(nullptr);
return;
}
std::string error;
scoped_refptr<const extensions::Extension> extension =
extensions::file_util::LoadExtension(
version_dir, extension_id, install_source, creation_flags, &error);
std::move(callback).Run(extension);
}
// Installs |extension| as a copy of an extension unpacked at |original_path|
// into |target_install_dir|.
// |profile| is the profile to which the extension is being installed.
// |callback| - called with the app loaded from the final installation path.
void InstallExtensionCopy(
const scoped_refptr<const extensions::Extension>& extension,
const base::FilePath& original_path,
const base::FilePath& target_install_dir,
Profile* profile,
bool updates_from_webstore_or_empty_update_url,
ExtensionCallback callback) {
base::FilePath target_dir = target_install_dir.Append(extension->id());
base::FilePath install_temp_dir =
extensions::file_util::GetInstallTempDir(target_dir);
auto extension_temp_dir = std::make_unique<base::ScopedTempDir>();
if (install_temp_dir.empty() ||
!extension_temp_dir->CreateUniqueTempDirUnderPath(install_temp_dir)) {
std::move(callback).Run(nullptr);
return;
}
// Copy the original extension path to a temp path to prevent
// ExtensionAssetsManager from deleting it (as InstallExtension renames the
// source path to a new location under the target install directory).
base::FilePath temp_copy =
extension_temp_dir->GetPath().Append(original_path.BaseName());
if (!base::CopyDirectory(original_path, temp_copy, true /* recursive */)) {
std::move(callback).Run(nullptr);
return;
}
// Note: |extension_temp_dir| is passed around to ensure it stays in scope
// until the app installation is done.
extensions::ExtensionAssetsManager::GetInstance()->InstallExtension(
extension.get(), temp_copy, target_install_dir, profile,
base::BindOnce(&LoadInstalledExtension, extension->id(),
extension->location(), extension->creation_flags(),
std::move(extension_temp_dir), std::move(callback)),
updates_from_webstore_or_empty_update_url);
}
} // namespace
AppManagerImpl::AppManagerImpl(const base::TickClock* tick_clock)
: tick_clock_(tick_clock) {}
AppManagerImpl::~AppManagerImpl() = default;
void AppManagerImpl::Initialize(
Profile* primary_profile,
LockScreenProfileCreator* lock_screen_profile_creator) {
DCHECK_EQ(State::kNotInitialized, state_);
DCHECK(primary_profile);
primary_profile_ = primary_profile;
lock_screen_profile_creator_ = lock_screen_profile_creator;
state_ = State::kInactive;
note_taking_helper_observation_.Observe(chromeos::NoteTakingHelper::Get());
lock_screen_profile_creator_->AddCreateProfileCallback(
base::BindOnce(&AppManagerImpl::OnLockScreenProfileLoaded,
weak_ptr_factory_.GetWeakPtr()));
}
void AppManagerImpl::OnLockScreenProfileLoaded() {
if (!lock_screen_profile_creator_->lock_screen_profile())
return;
DCHECK_NE(primary_profile_,
lock_screen_profile_creator_->lock_screen_profile());
// Do not use OTR profile for lock screen Chrome apps. This is important for
// profile usage in |LaunchLockScreenApp| - lock screen app background page
// runs in original, non off the record profile, so the launch event has to be
// dispatched to that profile. For other |lock_screen_profile_|, it makes no
// difference - the profile is used to get browser context keyed services, all
// of which redirect OTR profile to the original one.
lock_screen_profile_ =
lock_screen_profile_creator_->lock_screen_profile()->GetOriginalProfile();
CHECK(!chromeos::ProfileHelper::Get()->GetUserByProfile(lock_screen_profile_))
<< "Lock screen profile should not be associated with any users.";
UpdateLockScreenAppState();
}
void AppManagerImpl::Start(
const base::RepeatingClosure& note_taking_changed_callback) {
DCHECK_NE(State::kNotInitialized, state_);
app_changed_callback_ = note_taking_changed_callback;
if (state_ == State::kActive || state_ == State::kActivating)
return;
extensions_observation_.Observe(
extensions::ExtensionRegistry::Get(primary_profile_));
lock_screen_app_id_.clear();
std::string app_id = FindLockScreenAppId();
if (app_id.empty()) {
state_ = State::kAppUnavailable;
return;
}
state_ = AddAppToLockScreenProfile(app_id);
if (state_ == State::kActive || state_ == State::kActivating)
lock_screen_app_id_ = app_id;
}
void AppManagerImpl::Stop() {
DCHECK_NE(State::kNotInitialized, state_);
app_changed_callback_.Reset();
extensions_observation_.Reset();
available_lock_screen_app_reloads_ = 0;
if (state_ == State::kInactive)
return;
RemoveChromeAppFromLockScreenProfile(lock_screen_app_id_);
lock_screen_app_id_.clear();
state_ = State::kInactive;
}
bool AppManagerImpl::IsLockScreenAppAvailable() const {
return state_ == State::kActive && !lock_screen_app_id_.empty();
}
std::string AppManagerImpl::GetLockScreenAppId() const {
if (!IsLockScreenAppAvailable())
return std::string();
return lock_screen_app_id_;
}
bool AppManagerImpl::LaunchLockScreenApp() {
if (!IsLockScreenAppAvailable())
return false;
// TODO(crbug.com/1006642): Handle web apps here.
const extensions::Extension* app = GetChromeAppForLockScreenAppLaunch();
// If the app cannot be found at this point, it either got unexpectedly
// disabled, or it failed to reload (in case it was previously terminated).
// In either case, note taking should not be reported as available anymore.
if (!app) {
RemoveLockScreenAppDueToError();
return false;
}
auto action_data =
std::make_unique<extensions::api::app_runtime::ActionData>();
action_data->action_type =
extensions::api::app_runtime::ActionType::ACTION_TYPE_NEW_NOTE;
action_data->is_lock_screen_action = std::make_unique<bool>(true);
action_data->restore_last_action_state =
std::make_unique<bool>(primary_profile_->GetPrefs()->GetBoolean(
prefs::kRestoreLastLockScreenNote));
apps::LaunchPlatformAppWithAction(lock_screen_profile_, app,
std::move(action_data), base::FilePath());
return true;
}
void AppManagerImpl::OnExtensionLoaded(content::BrowserContext* browser_context,
const extensions::Extension* extension) {
if (browser_context == primary_profile_ &&
extension->id() ==
primary_profile_->GetPrefs()->GetString(prefs::kNoteTakingAppId)) {
UpdateLockScreenAppState();
}
}
void AppManagerImpl::OnExtensionUnloaded(
content::BrowserContext* browser_context,
const extensions::Extension* extension,
extensions::UnloadedExtensionReason reason) {
if (extension->id() != lock_screen_app_id_)
return;
if (browser_context == primary_profile_) {
UpdateLockScreenAppState();
} else if (browser_context == lock_screen_profile_) {
HandleLockScreenChromeAppUnload(reason);
}
}
void AppManagerImpl::OnExtensionUninstalled(
content::BrowserContext* browser_context,
const extensions::Extension* extension,
extensions::UninstallReason reason) {
// If the app is uninstalled from the lock screen apps profile, make sure
// it's not reported as available anymore.
if (browser_context == lock_screen_profile_ &&
extension->id() == lock_screen_app_id_) {
RemoveLockScreenAppDueToError();
}
}
void AppManagerImpl::OnAvailableNoteTakingAppsUpdated() {}
void AppManagerImpl::OnPreferredNoteTakingAppUpdated(Profile* profile) {
if (profile != primary_profile_)
return;
UpdateLockScreenAppState();
}
void AppManagerImpl::UpdateLockScreenAppState() {
if (state_ == State::kInactive)
return;
std::string app_id = FindLockScreenAppId();
if (app_id == lock_screen_app_id_)
return;
RemoveChromeAppFromLockScreenProfile(lock_screen_app_id_);
lock_screen_app_id_.clear();
state_ = AddAppToLockScreenProfile(app_id);
if (state_ == State::kActive || state_ == State::kActivating)
lock_screen_app_id_ = app_id;
if (!app_changed_callback_.is_null())
app_changed_callback_.Run();
}
std::string AppManagerImpl::FindLockScreenAppId() const {
// Note that lock screen does not currently support Android apps, so
// it's enough to only check the state of the preferred Chrome app.
std::unique_ptr<chromeos::NoteTakingAppInfo> note_taking_app =
chromeos::NoteTakingHelper::Get()->GetPreferredLockScreenAppInfo(
primary_profile_);
ActionAvailability availability =
GetLockScreenNoteTakingAvailability(note_taking_app.get());
// |lock_screen_profile_| is created only if a note taking app is available
// on the lock screen. If an app is not available, the profile is expected to
// be nullptr.
// If the app is available and the lock_screen_profile is not set, the profile
// might still be loading, and |FindLockScreenAppId| will be called
// again when the profile is loaded - until then, report to UMA that lock
// screen profile was not created at this point, and otherwise ignore the
// available app.
if (!lock_screen_profile_ && availability == ActionAvailability::kAvailable)
availability = ActionAvailability::kLockScreenProfileNotCreated;
UMA_HISTOGRAM_ENUMERATION(
"Apps.LockScreen.NoteTakingApp.AvailabilityOnScreenLock", availability,
ActionAvailability::kCount);
if (availability != ActionAvailability::kAvailable)
return std::string();
return note_taking_app->app_id;
}
AppManagerImpl::State AppManagerImpl::AddAppToLockScreenProfile(
const std::string& app_id) {
// TODO(crbug.com/1006642): First check if app_id is an installed web app.
extensions::ExtensionRegistry* primary_registry =
extensions::ExtensionRegistry::Get(primary_profile_);
const extensions::Extension* app =
primary_registry->enabled_extensions().GetByID(app_id);
if (!app)
return State::kAppUnavailable;
bool is_unpacked = extensions::Manifest::IsUnpackedLocation(app->location());
// Unpacked apps in lock screen profile will be loaded from their original
// file path, so their path will be the same as the primary profile app's.
// For the rest, the app will be copied to a location in the lock screen
// profile's extension install directory (using |InstallExtensionCopy|) - the
// exact final path is not known at this point, and will be set as part of
// |InstallExtensionCopy|.
base::FilePath lock_profile_app_path =
is_unpacked ? app->path() : base::FilePath();
std::string error;
scoped_refptr<extensions::Extension> lock_profile_app =
extensions::Extension::Create(lock_profile_app_path, app->location(),
*app->manifest()->value()->CreateDeepCopy(),
app->creation_flags(), app->id(), &error);
// While extension creation can fail in general, in this case the lock screen
// profile extension creation arguments come from an app already installed in
// a user profile. If the extension parameters were invalid, the app would not
// exist in a user profile, and thus |app| would be nullptr, which is not the
// case at this point.
DCHECK(lock_profile_app);
install_count_++;
if (is_unpacked) {
InstallAndEnableLockScreenChromeAppInLockScreenProfile(
lock_profile_app.get());
return State::kActive;
}
extensions::ExtensionService* lock_screen_service =
extensions::ExtensionSystem::Get(lock_screen_profile_)
->extension_service();
const GURL update_url =
extensions::ExtensionManagementFactory::GetForBrowserContext(
lock_screen_profile_)
->GetEffectiveUpdateURL(*lock_profile_app);
bool updates_from_webstore_or_empty_update_url =
update_url.is_empty() || extension_urls::IsWebstoreUpdateUrl(update_url);
extensions::GetExtensionFileTaskRunner()->PostTask(
FROM_HERE,
base::BindOnce(
&InstallExtensionCopy, lock_profile_app, app->path(),
lock_screen_service->install_directory(), lock_screen_profile_,
updates_from_webstore_or_empty_update_url,
base::BindOnce(
&InvokeCallbackOnTaskRunner,
base::BindOnce(
&AppManagerImpl::CompleteLockScreenChromeAppInstall,
weak_ptr_factory_.GetWeakPtr(), install_count_,
tick_clock_->NowTicks()),
base::ThreadTaskRunnerHandle::Get())));
return State::kActivating;
}
void AppManagerImpl::CompleteLockScreenChromeAppInstall(
int install_id,
base::TimeTicks install_start_time,
const scoped_refptr<const extensions::Extension>& app) {
UMA_HISTOGRAM_TIMES(
"Apps.LockScreen.NoteTakingApp.LockScreenInstallationDuration",
tick_clock_->NowTicks() - install_start_time);
// Bail out if the app manager is no longer waiting for this app's
// installation - the copied resources will be cleaned up when the (ephemeral)
// lock screen profile is destroyed.
if (install_id != install_count_ || state_ != State::kActivating)
return;
if (app) {
DCHECK_EQ(lock_screen_app_id_, app->id());
InstallAndEnableLockScreenChromeAppInLockScreenProfile(app.get());
state_ = State::kActive;
} else {
state_ = State::kAppUnavailable;
}
if (!app_changed_callback_.is_null())
app_changed_callback_.Run();
}
void AppManagerImpl::InstallAndEnableLockScreenChromeAppInLockScreenProfile(
const extensions::Extension* app) {
extensions::ExtensionService* lock_screen_service =
extensions::ExtensionSystem::Get(lock_screen_profile_)
->extension_service();
lock_screen_service->OnExtensionInstalled(
app, syncer::StringOrdinal(), extensions::kInstallFlagInstallImmediately);
lock_screen_service->EnableExtension(app->id());
available_lock_screen_app_reloads_ = kMaxLockScreenAppReloadsCount;
lock_screen_profile_extensions_observation_.Observe(
extensions::ExtensionRegistry::Get(lock_screen_profile_));
}
void AppManagerImpl::RemoveChromeAppFromLockScreenProfile(
const std::string& app_id) {
if (app_id.empty())
return;
lock_screen_profile_extensions_observation_.Reset();
extensions::ExtensionRegistry* lock_screen_registry =
extensions::ExtensionRegistry::Get(lock_screen_profile_);
if (!lock_screen_registry->GetExtensionById(
app_id, extensions::ExtensionRegistry::EVERYTHING)) {
return;
}
std::u16string error;
extensions::ExtensionSystem::Get(lock_screen_profile_)
->extension_service()
->UninstallExtension(
app_id, extensions::UNINSTALL_REASON_INTERNAL_MANAGEMENT, &error);
}
const extensions::Extension*
AppManagerImpl::GetChromeAppForLockScreenAppLaunch() {
// TODO(crbug.com/1006642): First check if app_id is an installed web app.
const extensions::ExtensionRegistry* extension_registry =
extensions::ExtensionRegistry::Get(lock_screen_profile_);
// Return the app, in case it's currently loaded.
const extensions::Extension* app = extension_registry->GetExtensionById(
lock_screen_app_id_, extensions::ExtensionRegistry::ENABLED);
if (app) {
ReportAppStatusOnAppLaunch(AppStatus::kEnabled);
return app;
}
// If the app has been terminated (which can happen due to an app crash),
// attempt a reload - otherwise, return nullptr to signal the app is
// unavailable.
app =
extension_registry->terminated_extensions().GetByID(lock_screen_app_id_);
if (!app) {
ReportAppStatusOnAppLaunch(AppStatus::kNotLoadedNotTerminated);
return nullptr;
}
if (available_lock_screen_app_reloads_ <= 0) {
ReportAppStatusOnAppLaunch(AppStatus::kTerminatedReloadLimitExceeded);
return nullptr;
}
available_lock_screen_app_reloads_--;
std::string error;
scoped_refptr<extensions::Extension> lock_profile_app =
extensions::Extension::Create(app->path(), app->location(),
*app->manifest()->value()->CreateDeepCopy(),
app->creation_flags(), app->id(), &error);
extensions::ExtensionService* extension_service =
extensions::ExtensionSystem::Get(lock_screen_profile_)
->extension_service();
extension_service->AddExtension(lock_profile_app.get());
extension_service->EnableExtension(lock_profile_app->id());
app = extension_registry->GetExtensionById(
lock_screen_app_id_, extensions::ExtensionRegistry::ENABLED);
ReportAppStatusOnAppLaunch(app ? AppStatus::kAppReloaded
: AppStatus::kAppReloadFailed);
return app;
}
void AppManagerImpl::ReportAppStatusOnAppLaunch(AppStatus status) {
UMA_HISTOGRAM_ENUMERATION(
"Apps.LockScreen.NoteTakingApp.AppStatusOnNoteLaunch", status,
AppStatus::kCount);
}
void AppManagerImpl::HandleLockScreenChromeAppUnload(
extensions::UnloadedExtensionReason reason) {
if (state_ != State::kActive && state_ != State::kActivating)
return;
AppUnloadStatus status = AppUnloadStatus::kNotTerminated;
if (reason == extensions::UnloadedExtensionReason::TERMINATE) {
status = available_lock_screen_app_reloads_ > 0
? AppUnloadStatus::kTerminatedReloadable
: AppUnloadStatus::kTerminatedReloadAttemptsExceeded;
}
UMA_HISTOGRAM_ENUMERATION(
"Apps.LockScreen.NoteTakingApp.LockScreenAppUnloaded", status,
AppUnloadStatus::kCount);
// If the app is terminated, it will be reloaded on the next app launch
// request - if the app cannot be reloaded (e.g. if it was unloaded for a
// different reason, or it was reloaded too many times already), change the
// app managet to an error state. This will inform the app manager's user
// that lock screen note action is not available anymore.
if (status != AppUnloadStatus::kTerminatedReloadable)
RemoveLockScreenAppDueToError();
if (status != AppUnloadStatus::kNotTerminated) {
UMA_HISTOGRAM_ENUMERATION(
"Apps.LockScreen.NoteTakingApp.ReloadCountOnAppTermination",
kMaxLockScreenAppReloadsCount - available_lock_screen_app_reloads_,
kMaxLockScreenAppReloadsCount + 1);
}
}
void AppManagerImpl::RemoveLockScreenAppDueToError() {
if (state_ != State::kActive && state_ != State::kActivating)
return;
RemoveChromeAppFromLockScreenProfile(lock_screen_app_id_);
lock_screen_app_id_.clear();
state_ = State::kInactive;
if (!app_changed_callback_.is_null())
app_changed_callback_.Run();
}
} // namespace lock_screen_apps