blob: 92d6a65a33e9d4eecdb60979030602741331ff96 [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/chromeos/lock_screen_apps/state_controller.h"
#include <utility>
#include "ash/public/cpp/stylus_utils.h"
#include "ash/public/interfaces/constants.mojom.h"
#include "base/base64.h"
#include "base/bind.h"
#include "base/command_line.h"
#include "base/metrics/histogram_macros.h"
#include "base/path_service.h"
#include "base/strings/string16.h"
#include "base/time/default_tick_clock.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/chromeos/lock_screen_apps/app_manager_impl.h"
#include "chrome/browser/chromeos/lock_screen_apps/app_window_metrics_tracker.h"
#include "chrome/browser/chromeos/lock_screen_apps/first_app_run_toast_manager.h"
#include "chrome/browser/chromeos/lock_screen_apps/focus_cycler_delegate.h"
#include "chrome/browser/chromeos/lock_screen_apps/lock_screen_profile_creator_impl.h"
#include "chrome/browser/chromeos/note_taking_helper.h"
#include "chrome/browser/chromeos/profiles/profile_helper.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/chrome_paths.h"
#include "chromeos/constants/chromeos_switches.h"
#include "chromeos/dbus/dbus_thread_manager.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "components/session_manager/core/session_manager.h"
#include "components/user_manager/user.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/service_manager_connection.h"
#include "crypto/symmetric_key.h"
#include "extensions/browser/api/lock_screen_data/lock_screen_item_storage.h"
#include "extensions/browser/app_window/app_window.h"
#include "extensions/browser/app_window/native_app_window.h"
#include "extensions/common/extension.h"
#include "services/service_manager/public/cpp/connector.h"
#include "ui/events/devices/input_device_manager.h"
#include "ui/wm/core/window_animations.h"
using ash::mojom::CloseLockScreenNoteReason;
using ash::mojom::LockScreenNoteOrigin;
using ash::mojom::TrayActionState;
namespace lock_screen_apps {
namespace {
// Key for user pref that contains the 256 bit AES key that should be used to
// encrypt persisted user data created on the lock screen.
constexpr char kDataCryptoKeyPref[] = "lockScreenAppDataCryptoKey";
StateController* g_state_controller_instance = nullptr;
// Generates a random 256 bit AES key. Returns an empty string on error.
std::string GenerateCryptoKey() {
std::unique_ptr<crypto::SymmetricKey> symmetric_key =
crypto::SymmetricKey::GenerateRandomKey(crypto::SymmetricKey::AES, 256);
if (!symmetric_key)
return "";
return symmetric_key->key();
}
} // namespace
// static
StateController* StateController::Get() {
DCHECK(g_state_controller_instance);
return g_state_controller_instance;
}
// static
void StateController::RegisterProfilePrefs(PrefRegistrySimple* registry) {
registry->RegisterStringPref(kDataCryptoKeyPref, "");
}
StateController::StateController()
: binding_(this),
note_window_observer_(this),
app_window_observer_(this),
session_observer_(this),
input_devices_observer_(this),
power_manager_client_observer_(this),
weak_ptr_factory_(this) {
DCHECK(!g_state_controller_instance);
g_state_controller_instance = this;
}
StateController::~StateController() {
DCHECK_EQ(g_state_controller_instance, this);
g_state_controller_instance = nullptr;
}
void StateController::SetTrayActionPtrForTesting(
ash::mojom::TrayActionPtr tray_action_ptr) {
tray_action_ptr_ = std::move(tray_action_ptr);
}
void StateController::FlushTrayActionForTesting() {
tray_action_ptr_.FlushForTesting();
}
void StateController::SetReadyCallbackForTesting(
const base::Closure& ready_callback) {
DCHECK(ready_callback_.is_null());
ready_callback_ = ready_callback;
}
void StateController::SetTickClockForTesting(const base::TickClock* clock) {
DCHECK(!tick_clock_);
tick_clock_ = clock;
}
void StateController::SetAppManagerForTesting(
std::unique_ptr<AppManager> app_manager) {
DCHECK(!app_manager_);
app_manager_ = std::move(app_manager);
}
void StateController::SetLockScreenLockScreenProfileCreatorForTesting(
std::unique_ptr<LockScreenProfileCreator> profile_creator) {
DCHECK(!lock_screen_profile_creator_);
lock_screen_profile_creator_ = std::move(profile_creator);
}
void StateController::Initialize() {
if (!tick_clock_)
tick_clock_ = base::DefaultTickClock::GetInstance();
// The tray action ptr might be set previously if the client was being created
// for testing.
if (!tray_action_ptr_) {
service_manager::Connector* connector =
content::ServiceManagerConnection::GetForProcess()->GetConnector();
connector->BindInterface(ash::mojom::kServiceName, &tray_action_ptr_);
}
ash::mojom::TrayActionClientPtr client;
binding_.Bind(mojo::MakeRequest(&client));
tray_action_ptr_->SetClient(std::move(client), lock_screen_note_state_);
}
void StateController::SetPrimaryProfile(Profile* profile) {
const user_manager::User* user =
chromeos::ProfileHelper::Get()->GetUserByProfile(profile);
if (!user || !user->HasGaiaAccount()) {
if (!ready_callback_.is_null())
std::move(ready_callback_).Run();
return;
}
std::string key;
if (!GetUserCryptoKey(profile, &key)) {
LOG(ERROR) << "Failed to get crypto key for user lock screen apps.";
return;
}
InitializeWithCryptoKey(profile, key);
}
void StateController::Shutdown() {
session_observer_.RemoveAll();
lock_screen_data_.reset();
if (app_manager_) {
app_manager_->Stop();
ResetNoteTakingWindowAndMoveToNextState(
true /*close_window*/, CloseLockScreenNoteReason::kShutdown);
app_manager_.reset();
}
first_app_run_toast_manager_.reset();
lock_screen_profile_creator_.reset();
focus_cycler_delegate_ = nullptr;
power_manager_client_observer_.RemoveAll();
input_devices_observer_.RemoveAll();
binding_.Close();
weak_ptr_factory_.InvalidateWeakPtrs();
}
bool StateController::GetUserCryptoKey(Profile* profile, std::string* key) {
*key = profile->GetPrefs()->GetString(kDataCryptoKeyPref);
if (!key->empty() && base::Base64Decode(*key, key))
return true;
*key = GenerateCryptoKey();
if (key->empty())
return false;
std::string base64_encoded_key;
base::Base64Encode(*key, &base64_encoded_key);
profile->GetPrefs()->SetString(kDataCryptoKeyPref, base64_encoded_key);
return true;
}
void StateController::InitializeWithCryptoKey(Profile* profile,
const std::string& crypto_key) {
base::FilePath base_path;
if (!base::PathService::Get(chrome::DIR_USER_DATA, &base_path)) {
LOG(ERROR) << "Failed to get base storage dir for lock screen app data.";
return;
}
lock_screen_data_ =
std::make_unique<extensions::lock_screen_data::LockScreenItemStorage>(
profile, g_browser_process->local_state(), crypto_key,
base_path.AppendASCII("lock_screen_app_data"),
base_path.AppendASCII("lock_screen_app_data_v2"));
lock_screen_data_->SetSessionLocked(false);
chromeos::NoteTakingHelper::Get()->SetProfileWithEnabledLockScreenApps(
profile);
// Lock screen profile creator might have been set by a test.
if (!lock_screen_profile_creator_) {
lock_screen_profile_creator_ =
std::make_unique<LockScreenProfileCreatorImpl>(profile, tick_clock_);
}
lock_screen_profile_creator_->Initialize();
// App manager might have been set previously by a test.
if (!app_manager_)
app_manager_ = std::make_unique<AppManagerImpl>(tick_clock_);
app_manager_->Initialize(profile, lock_screen_profile_creator_.get());
first_app_run_toast_manager_ =
std::make_unique<FirstAppRunToastManager>(profile);
input_devices_observer_.Add(ui::InputDeviceManager::GetInstance());
// Do not start state controller if stylus input is not present as lock
// screen notes apps are geared towards stylus.
// State controller will observe inpt device changes and continue
// initialization if stylus input is found.
if (!ash::stylus_utils::HasStylusInput()) {
stylus_input_missing_ = true;
if (!ready_callback_.is_null())
std::move(ready_callback_).Run();
return;
}
InitializeWithStylusInputPresent();
}
void StateController::InitializeWithStylusInputPresent() {
stylus_input_missing_ = false;
power_manager_client_observer_.Add(chromeos::PowerManagerClient::Get());
session_observer_.Add(session_manager::SessionManager::Get());
OnSessionStateChanged();
// SessionController is fully initialized at this point.
if (!ready_callback_.is_null())
std::move(ready_callback_).Run();
}
void StateController::AddObserver(StateObserver* observer) {
observers_.AddObserver(observer);
}
void StateController::RemoveObserver(StateObserver* observer) {
observers_.RemoveObserver(observer);
}
void StateController::SetFocusCyclerDelegate(FocusCyclerDelegate* delegate) {
DCHECK(!focus_cycler_delegate_ || !delegate);
if (focus_cycler_delegate_ && note_app_window_)
focus_cycler_delegate_->UnregisterLockScreenAppFocusHandler();
focus_cycler_delegate_ = delegate;
if (focus_cycler_delegate_ && note_app_window_) {
focus_cycler_delegate_->RegisterLockScreenAppFocusHandler(base::Bind(
&StateController::FocusAppWindow, weak_ptr_factory_.GetWeakPtr()));
}
}
TrayActionState StateController::GetLockScreenNoteState() const {
return lock_screen_note_state_;
}
void StateController::RequestNewLockScreenNote(LockScreenNoteOrigin origin) {
if (lock_screen_note_state_ != TrayActionState::kAvailable)
return;
DCHECK(app_manager_->IsNoteTakingAppAvailable());
UMA_HISTOGRAM_ENUMERATION("Apps.LockScreen.NoteTakingApp.LaunchRequestReason",
origin);
// Update state to launching even if app fails to launch - this is to notify
// listeners that a lock screen note request was handled.
UpdateLockScreenNoteState(TrayActionState::kLaunching);
if (!app_manager_->LaunchNoteTaking()) {
UpdateLockScreenNoteState(TrayActionState::kAvailable);
return;
}
note_app_window_metrics_->AppLaunchRequested();
}
void StateController::CloseLockScreenNote(CloseLockScreenNoteReason reason) {
ResetNoteTakingWindowAndMoveToNextState(true /*close_window*/, reason);
}
void StateController::OnSessionStateChanged() {
if (!session_manager::SessionManager::Get()->IsScreenLocked()) {
lock_screen_data_->SetSessionLocked(false);
app_manager_->Stop();
ResetNoteTakingWindowAndMoveToNextState(
true /*close_window*/, CloseLockScreenNoteReason::kSessionUnlock);
note_app_window_metrics_.reset();
return;
}
// base::Unretained is safe here because |app_manager_| is owned by |this|,
// and the callback will not be invoked after |app_manager_| goes out of
// scope.
app_manager_->Start(
base::Bind(&StateController::OnNoteTakingAvailabilityChanged,
base::Unretained(this)));
note_app_window_metrics_ =
std::make_unique<AppWindowMetricsTracker>(tick_clock_);
lock_screen_data_->SetSessionLocked(true);
OnNoteTakingAvailabilityChanged();
}
void StateController::OnWindowVisibilityChanged(aura::Window* window,
bool visible) {
if (lock_screen_note_state_ != TrayActionState::kLaunching)
return;
if (window != note_app_window_->GetNativeWindow() || !window->IsVisible())
return;
note_window_observer_.Remove(window);
UpdateLockScreenNoteState(TrayActionState::kActive);
if (focus_cycler_delegate_) {
focus_cycler_delegate_->RegisterLockScreenAppFocusHandler(base::Bind(
&StateController::FocusAppWindow, weak_ptr_factory_.GetWeakPtr()));
}
}
void StateController::OnWindowDestroying(aura::Window* window) {
if (window != note_app_window_->GetNativeWindow())
return;
ResetNoteTakingWindowAndMoveToNextState(
false /*close_window*/, CloseLockScreenNoteReason::kAppWindowClosed);
}
void StateController::OnAppWindowAdded(extensions::AppWindow* app_window) {
if (note_app_window_ != app_window)
return;
note_window_observer_.Add(note_app_window_->GetNativeWindow());
first_app_run_toast_manager_->RunForAppWindow(note_app_window_);
note_app_window_metrics_->AppWindowCreated(app_window);
}
void StateController::OnAppWindowRemoved(extensions::AppWindow* app_window) {
if (note_app_window_ != app_window)
return;
ResetNoteTakingWindowAndMoveToNextState(
false /*close_window*/, CloseLockScreenNoteReason::kAppWindowClosed);
}
void StateController::OnInputDeviceConfigurationChanged(
uint8_t input_device_types) {
if ((input_device_types & ui::InputDeviceEventObserver::kTouchscreen) &&
stylus_input_missing_ && ash::stylus_utils::HasStylusInput()) {
InitializeWithStylusInputPresent();
}
}
void StateController::SuspendImminent(
power_manager::SuspendImminent::Reason reason) {
ResetNoteTakingWindowAndMoveToNextState(true /*close_window*/,
CloseLockScreenNoteReason::kSuspend);
}
extensions::AppWindow* StateController::CreateAppWindowForLockScreenAction(
content::BrowserContext* context,
const extensions::Extension* extension,
extensions::api::app_runtime::ActionType action,
std::unique_ptr<extensions::AppDelegate> app_delegate) {
if (action != extensions::api::app_runtime::ACTION_TYPE_NEW_NOTE)
return nullptr;
if (note_app_window_)
return nullptr;
if (lock_screen_note_state_ != TrayActionState::kLaunching)
return nullptr;
// StateController should not be able to get into kLaunching state if the
// lock screen profile has not been loaded, and |lock_screen_profile_creator_|
// has |lock_screen_profile| set to null - if the lock screen profile is not
// loaded, |app_manager_| should not report that note taking app is available,
// so state controller should not allow note launch attempt.
// Thus, it should be safe to assume lock screen profile is set at this point.
DCHECK(lock_screen_profile_creator_->lock_screen_profile());
if (!lock_screen_profile_creator_->lock_screen_profile()->IsSameProfile(
Profile::FromBrowserContext(context))) {
return nullptr;
}
if (!extension || app_manager_->GetNoteTakingAppId() != extension->id())
return nullptr;
// The ownership of the window is passed to the caller of this method.
note_app_window_ =
new extensions::AppWindow(context, app_delegate.release(), extension);
app_window_observer_.Add(extensions::AppWindowRegistry::Get(
lock_screen_profile_creator_->lock_screen_profile()));
return note_app_window_;
}
bool StateController::HandleTakeFocus(content::WebContents* web_contents,
bool reverse) {
if (!focus_cycler_delegate_ ||
GetLockScreenNoteState() != TrayActionState::kActive ||
note_app_window_->web_contents() != web_contents) {
return false;
}
focus_cycler_delegate_->HandleLockScreenAppFocusOut(reverse);
return true;
}
void StateController::OnNoteTakingAvailabilityChanged() {
if (!app_manager_->IsNoteTakingAppAvailable() ||
(note_app_window_ && note_app_window_->GetExtension()->id() !=
app_manager_->GetNoteTakingAppId())) {
ResetNoteTakingWindowAndMoveToNextState(
true /*close_window*/,
CloseLockScreenNoteReason::kAppLockScreenSupportDisabled);
return;
}
if (GetLockScreenNoteState() == TrayActionState::kNotAvailable)
UpdateLockScreenNoteState(TrayActionState::kAvailable);
}
void StateController::FocusAppWindow(bool reverse) {
// If the app window is not active, pass the focus on to the delegate..
if (GetLockScreenNoteState() != TrayActionState::kActive) {
focus_cycler_delegate_->HandleLockScreenAppFocusOut(reverse);
return;
}
note_app_window_->web_contents()->FocusThroughTabTraversal(reverse);
note_app_window_->GetBaseWindow()->Activate();
note_app_window_->web_contents()->Focus();
}
void StateController::ResetNoteTakingWindowAndMoveToNextState(
bool close_window,
CloseLockScreenNoteReason reason) {
note_window_observer_.RemoveAll();
app_window_observer_.RemoveAll();
if (first_app_run_toast_manager_)
first_app_run_toast_manager_->Reset();
if (note_app_window_metrics_)
note_app_window_metrics_->Reset();
if (lock_screen_note_state_ != TrayActionState::kAvailable &&
lock_screen_note_state_ != TrayActionState::kNotAvailable) {
UMA_HISTOGRAM_ENUMERATION(
"Apps.LockScreen.NoteTakingApp.NoteTakingExitReason", reason);
}
if (focus_cycler_delegate_ &&
lock_screen_note_state_ == TrayActionState::kActive) {
focus_cycler_delegate_->UnregisterLockScreenAppFocusHandler();
}
if (note_app_window_) {
if (close_window && note_app_window_->GetBaseWindow()) {
// Whenever we close the window we want to immediately hide it without
// animating, as the underlying UI implements a special animation. If we
// also animate the window the animations will conflict.
::wm::SetWindowVisibilityAnimationTransition(
note_app_window_->GetNativeWindow(), ::wm::ANIMATE_NONE);
note_app_window_->GetBaseWindow()->Close();
}
note_app_window_ = nullptr;
}
UpdateLockScreenNoteState(app_manager_ &&
app_manager_->IsNoteTakingAppAvailable()
? TrayActionState::kAvailable
: TrayActionState::kNotAvailable);
}
bool StateController::UpdateLockScreenNoteState(TrayActionState state) {
const TrayActionState old_state = GetLockScreenNoteState();
if (old_state == state)
return false;
lock_screen_note_state_ = state;
NotifyLockScreenNoteStateChanged();
return true;
}
void StateController::NotifyLockScreenNoteStateChanged() {
for (auto& observer : observers_)
observer.OnLockScreenNoteStateChanged(lock_screen_note_state_);
tray_action_ptr_->UpdateLockScreenNoteState(lock_screen_note_state_);
}
} // namespace lock_screen_apps