blob: 38b7982b73a29c0b591b72cbbf9f5fe99bfd345c [file] [log] [blame]
// Copyright 2022 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/enterprise/idle/action.h"
#include <algorithm>
#include <cstring>
#include <utility>
#include "base/callback_list.h"
#include "base/check_is_test.h"
#include "base/containers/flat_map.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/scoped_observation.h"
#include "base/task/single_thread_task_runner.h"
#include "chrome/browser/browsing_data/chrome_browsing_data_remover_constants.h"
#include "chrome/browser/enterprise/idle/action_runner.h"
#include "chrome/browser/enterprise/idle/idle_service_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "components/enterprise/idle/idle_pref_names.h"
#include "components/enterprise/idle/metrics.h"
#include "components/prefs/pref_service.h"
#include "content/public/browser/browsing_data_remover.h"
#include "content/public/browser/web_contents.h"
#if BUILDFLAG(IS_ANDROID)
#include "chrome/browser/ui/android/tab_model/tab_model.h"
#include "chrome/browser/ui/android/tab_model/tab_model_list.h"
#else
#include "chrome/browser/enterprise/idle/dialog_manager.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_list.h"
#include "chrome/browser/ui/idle_bubble.h"
#include "chrome/browser/ui/profiles/profile_picker.h"
#endif // BUILDFLAG(IS_ANDROID)
namespace enterprise_idle {
namespace {
#if !BUILDFLAG(IS_ANDROID)
bool ProfileHasBrowsers(const Profile* profile) {
DCHECK(profile);
profile = profile->GetOriginalProfile();
return std::ranges::any_of(
*BrowserList::GetInstance(), [profile](Browser* browser) {
return browser->profile()->GetOriginalProfile() == profile;
});
}
// Wrapper Action for DialogManager. Shows a 30s warning dialog, shared across
// profiles.
class ShowDialogAction : public Action {
public:
explicit ShowDialogAction(base::flat_set<ActionType> action_types)
: Action(static_cast<int>(ActionType::kShowDialog)),
action_types_(std::move(action_types)) {}
void Run(Profile* profile, Continuation continuation) override {
base::TimeDelta timeout =
IdleServiceFactory::GetForBrowserContext(profile)->GetTimeout();
continuation_ = std::move(continuation);
// Action object's lifetime extends until it calls `continuation_`, so
// passing `this` as a raw pointer is safe.
base::CallbackListSubscription subscription =
DialogManager::GetInstance()->MaybeShowDialog(
profile, timeout, action_types_,
base::BindOnce(&ShowDialogAction::OnDialogFinished,
base::Unretained(this)));
if (subscription) {
// If there is no dialog to show, MaybeShowDialog() resolves immediately
// and we destroy this object via OnCloseFinished(). This if guards
// against a use-after-free.
subscription_ = std::move(subscription);
}
}
bool ShouldNotifyUserOfPendingDestructiveAction(Profile* profile) override {
NOTREACHED(); // Should only be called in ActionFactory::Build().
}
private:
void OnDialogFinished(bool expired) {
std::move(continuation_).Run(/*success=*/expired);
}
base::flat_set<ActionType> action_types_;
Action::Continuation continuation_;
base::CallbackListSubscription subscription_;
};
// Action that closes all browsers for a Profile.
class CloseBrowsersAction : public Action {
public:
CloseBrowsersAction()
: Action(static_cast<int>(ActionType::kCloseBrowsers)) {}
// Action:
void Run(Profile* profile, Continuation continuation) override {
if (!ProfileHasBrowsers(profile)) {
// No browsers for this profile, so it'd be a no-op. Finish immediately.
std::move(continuation).Run(/*success=*/true);
return;
}
continuation_ = std::move(continuation);
// TODO(crbug.com/40222234): Get customer feedback on whether
// skip_beforeunload should be true or false.
BrowserList::CloseAllBrowsersWithProfile(
profile,
base::BindRepeating(&CloseBrowsersAction::OnCloseSuccess,
base::Unretained(this)),
base::BindRepeating(&CloseBrowsersAction::OnCloseAborted,
base::Unretained(this)),
/*skip_beforeunload=*/true);
}
bool ShouldNotifyUserOfPendingDestructiveAction(Profile* profile) override {
// If there are no browsers, closing is a no-op. No need for a dialog then.
return profile && ProfileHasBrowsers(profile);
}
private:
void OnCloseSuccess(const base::FilePath& profile_dir) {
metrics::RecordActionsSuccess(
metrics::IdleTimeoutActionType::kCloseBrowsers, true);
std::move(continuation_).Run(/*success=*/true);
}
void OnCloseAborted(const base::FilePath& profile_dir) {
metrics::RecordActionsSuccess(
metrics::IdleTimeoutActionType::kCloseBrowsers, false);
std::move(continuation_).Run(/*success=*/false);
}
Action::Continuation continuation_;
base::CallbackListSubscription subscription_;
};
// Action that shows the Profile Picker.
class ShowProfilePickerAction : public Action {
public:
ShowProfilePickerAction()
: Action(static_cast<int>(ActionType::kShowProfilePicker)) {}
// Action:
void Run(Profile* profile, Continuation continuation) override {
ProfilePicker::Show(ProfilePicker::Params::FromEntryPoint(
ProfilePicker::EntryPoint::kProfileIdle));
metrics::RecordActionsSuccess(
metrics::IdleTimeoutActionType::kShowProfilePicker, true);
std::move(continuation).Run(true);
}
bool ShouldNotifyUserOfPendingDestructiveAction(Profile* profile) override {
return false;
}
};
#endif // !BUILDFLAG(IS_ANDROID)
// Action that clears one or more types of data via BrowsingDataRemover.
// Multiple data types may be grouped into a single ClearBrowsingDataAction
// object.
//
// TODO(crbug.com/40840688): Call ChromeBrowsingDataLifetimeManager, instead of
// BrowsingDataRemover directly? Especially if we add a keepalive, or use
// kClearBrowsingDataOnExitDeletionPending...
class ClearBrowsingDataAction : public Action,
content::BrowsingDataRemover::Observer {
public:
explicit ClearBrowsingDataAction(
base::flat_set<ActionType> action_types,
content::BrowsingDataRemover* browsing_data_remover_for_testing)
: Action(static_cast<int>(ActionType::kClearBrowsingHistory)),
action_types_(std::move(action_types)),
browsing_data_remover_for_testing_(browsing_data_remover_for_testing) {}
~ClearBrowsingDataAction() override = default;
// Action:
void Run(Profile* profile, Continuation continuation) override {
auto* remover = profile->GetBrowsingDataRemover();
if (browsing_data_remover_for_testing_) {
remover = browsing_data_remover_for_testing_.get();
}
observation_.Observe(remover);
continuation_ = std::move(continuation);
// Action object's lifetime extends until it calls `continuation_`, so
// passing `this` as a raw pointer is safe.
deletion_start_time_ = base::TimeTicks::Now();
remover->RemoveAndReply(base::Time(), base::Time::Max(), GetRemoveMask(),
GetOriginTypeMask(), this);
// TODO(crbug.com/40840688): Add a pair of keepalives?
}
bool ShouldNotifyUserOfPendingDestructiveAction(Profile* profile) override {
#if BUILDFLAG(IS_ANDROID)
return true;
#else
return profile && ProfileHasBrowsers(profile);
#endif
}
// content::BrowsingDataRemoverObserver::Observer:
void OnBrowsingDataRemoverDone(uint64_t failed_data_types) override {
bool success = failed_data_types == 0;
observation_.Reset();
metrics::RecordActionsSuccess(
metrics::IdleTimeoutActionType::kClearBrowsingData, success);
metrics::RecordIdleTimeoutActionTimeTaken(
metrics::IdleTimeoutActionType::kClearBrowsingData,
base::TimeTicks::Now() - deletion_start_time_);
std::move(continuation_).Run(success);
}
private:
uint64_t GetRemoveMask() const {
using content::BrowsingDataRemover;
static const std::pair<ActionType, uint64_t> entries[] = {
#if !BUILDFLAG(IS_ANDROID)
{ActionType::kClearDownloadHistory,
BrowsingDataRemover::DATA_TYPE_DOWNLOADS},
{ActionType::kClearHostedAppData,
chrome_browsing_data_remover::DATA_TYPE_SITE_DATA},
#endif // !BUILDFLAG(IS_ANDROID)
{ActionType::kClearBrowsingHistory,
chrome_browsing_data_remover::DATA_TYPE_HISTORY},
{ActionType::kClearCookiesAndOtherSiteData,
chrome_browsing_data_remover::DATA_TYPE_SITE_DATA},
{ActionType::kClearCachedImagesAndFiles,
BrowsingDataRemover::DATA_TYPE_CACHE},
{ActionType::kClearPasswordSignin,
chrome_browsing_data_remover::DATA_TYPE_PASSWORDS},
{ActionType::kClearAutofill,
chrome_browsing_data_remover::DATA_TYPE_FORM_DATA},
{ActionType::kClearSiteSettings,
chrome_browsing_data_remover::DATA_TYPE_CONTENT_SETTINGS}
};
uint64_t result = 0;
for (const auto& [action_type, mask] : entries) {
if (base::Contains(action_types_, action_type)) {
result |= mask;
}
}
return result;
}
uint64_t GetOriginTypeMask() const {
using content::BrowsingDataRemover;
uint64_t result = 0;
if (base::Contains(action_types_,
ActionType::kClearCookiesAndOtherSiteData)) {
result |= BrowsingDataRemover::ORIGIN_TYPE_UNPROTECTED_WEB;
}
#if !BUILDFLAG(IS_ANDROID)
if (base::Contains(action_types_, ActionType::kClearHostedAppData)) {
result |= BrowsingDataRemover::ORIGIN_TYPE_PROTECTED_WEB;
}
#endif // !BUILDFLAG(IS_ANDROID)
return result;
}
base::TimeTicks deletion_start_time_;
base::flat_set<ActionType> action_types_;
raw_ptr<content::BrowsingDataRemover, DanglingUntriaged>
browsing_data_remover_for_testing_;
base::ScopedObservation<content::BrowsingDataRemover,
content::BrowsingDataRemover::Observer>
observation_{this};
Continuation continuation_;
};
class ReloadPagesAction : public Action {
public:
ReloadPagesAction() : Action(static_cast<int>(ActionType::kReloadPages)) {}
void Run(Profile* profile, Continuation continuation) override {
#if BUILDFLAG(IS_ANDROID)
// This covers regular tabs, PWAs, and CCTs.
for (TabModel* model : TabModelList::models()) {
if (model->GetProfile() != profile) {
continue; // Deliberately ignore incognito.
}
#else
// This covers regular tabs and PWAs.
for (Browser* browser : *BrowserList::GetInstance()) {
TabStripModel* model = browser->tab_strip_model();
if (model->profile() != profile) {
continue; // Deliberately ignore incognito.
}
#endif // BUILDFLAG(IS_ANDROID)
for (int i = 0; i < model->GetTabCount(); i++) {
model->GetWebContentsAt(i)->GetController().Reload(
content::ReloadType::NORMAL,
/*check_for_repost=*/true);
}
}
metrics::RecordActionsSuccess(metrics::IdleTimeoutActionType::kReloadPages,
true);
std::move(continuation).Run(/*success=*/true);
}
bool ShouldNotifyUserOfPendingDestructiveAction(Profile* profile) override {
#if BUILDFLAG(IS_ANDROID)
return true;
#else
return profile && ProfileHasBrowsers(profile);
#endif
}
};
#if !BUILDFLAG(IS_ANDROID)
// Shows a bubble anchored to the 3-dot menu after other actions are finished.
class ShowBubbleAction : public Action {
public:
explicit ShowBubbleAction(base::flat_set<ActionType> action_types)
: Action(static_cast<int>(ActionType::kShowBubble)),
action_types_(std::move(action_types)) {}
void Run(Profile* profile, Continuation continuation) override {
Browser* browser = BrowserList::GetInstance()->GetLastActive();
profile->GetPrefs()->SetBoolean(prefs::kIdleTimeoutShowBubbleOnStartup,
true);
if (browser && browser->IsActive() && browser->profile() == profile &&
!base::Contains(action_types_, ActionType::kCloseBrowsers)) {
// A browser for this profile has focus. Show the bubble there.
ShowIdleBubble(
browser,
IdleServiceFactory::GetForBrowserContext(profile)->GetTimeout(),
ActionsToActionSet(action_types_),
base::BindOnce(&ShowBubbleAction::OnClose, browser->AsWeakPtr()));
} else {
// No active browser for this profile. Show the bubble when a browser
// gains focus, or on next startup. Let IdleService::BrowserObserver do
// it.
}
std::move(continuation).Run(true);
}
bool ShouldNotifyUserOfPendingDestructiveAction(Profile* profile) override {
return false;
}
private:
static void OnClose(base::WeakPtr<Browser> browser) {
if (!browser) {
return;
}
browser->profile()->GetPrefs()->SetBoolean(
prefs::kIdleTimeoutShowBubbleOnStartup, false);
}
base::flat_set<ActionType> action_types_;
};
#endif // !BUILDFLAG(IS_ANDROID)
} // namespace
Action::Action(int priority) : priority_(priority) {}
Action::~Action() = default;
bool ActionFactory::CompareActionsByPriority::operator()(
const std::unique_ptr<Action>& a,
const std::unique_ptr<Action>& b) const {
return a->priority() > b->priority();
}
// static
ActionFactory* ActionFactory::GetInstance() {
static base::NoDestructor<ActionFactory> instance;
return instance.get();
}
ActionFactory::ActionQueue ActionFactory::Build(
Profile* profile,
const std::vector<ActionType>& action_types) {
std::vector<std::unique_ptr<Action>> actions;
base::flat_set<ActionType> clear_actions;
for (auto action_type : action_types) {
switch (action_type) {
#if !BUILDFLAG(IS_ANDROID)
case ActionType::kCloseBrowsers:
actions.push_back(std::make_unique<CloseBrowsersAction>());
break;
case ActionType::kShowProfilePicker:
actions.push_back(std::make_unique<ShowProfilePickerAction>());
break;
#endif // !BUILDFLAG(IS_ANDROID)
// "clear_*" actions are all grouped into a single Action object. Collect
// them in a flat_set<>, and create the shared object once we have the
// entire collection.
#if !BUILDFLAG(IS_ANDROID)
case ActionType::kClearDownloadHistory:
case ActionType::kClearHostedAppData:
#endif // !BUILDFLAG(IS_ANDROID)
case ActionType::kClearBrowsingHistory:
case ActionType::kClearCookiesAndOtherSiteData:
case ActionType::kClearCachedImagesAndFiles:
case ActionType::kClearPasswordSignin:
case ActionType::kClearAutofill:
case ActionType::kClearSiteSettings:
clear_actions.insert(action_type);
break;
case ActionType::kReloadPages:
actions.push_back(std::make_unique<ReloadPagesAction>());
break;
default:
// TODO(crbug.com/40222234): Perform validation in the `PolicyHandler`.
NOTREACHED();
}
}
if (!clear_actions.empty()) {
// Merge "clear_*" actions into a single Action.
actions.push_back(std::make_unique<ClearBrowsingDataAction>(
std::move(clear_actions), browsing_data_remover_for_testing_));
}
#if !BUILDFLAG(IS_ANDROID)
bool needs_dialog = std::ranges::any_of(actions, [profile](const auto& a) {
return a->ShouldNotifyUserOfPendingDestructiveAction(profile);
});
if (needs_dialog) {
actions.push_back(std::make_unique<ShowDialogAction>(action_types));
actions.push_back(std::make_unique<ShowBubbleAction>(action_types));
}
#endif // !BUILDFLAG(IS_ANDROID)
return ActionQueue(ActionQueue::value_compare(), std::move(actions));
}
ActionFactory::ActionFactory() = default;
ActionFactory::~ActionFactory() = default;
void ActionFactory::SetBrowsingDataRemoverForTesting(
content::BrowsingDataRemover* remover) {
CHECK_IS_TEST();
browsing_data_remover_for_testing_ = remover;
}
#if !BUILDFLAG(IS_ANDROID)
IdleDialog::ActionSet ActionsToActionSet(
const base::flat_set<ActionType>& action_types) {
IdleDialog::ActionSet action_set = {.close = false, .clear = false};
for (ActionType action_type : action_types) {
switch (action_type) {
case ActionType::kCloseBrowsers:
action_set.close = true;
break;
case ActionType::kShowDialog:
case ActionType::kShowProfilePicker:
case ActionType::kShowBubble:
break;
case ActionType::kClearBrowsingHistory:
case ActionType::kClearDownloadHistory:
case ActionType::kClearCookiesAndOtherSiteData:
case ActionType::kClearCachedImagesAndFiles:
case ActionType::kClearPasswordSignin:
case ActionType::kClearAutofill:
case ActionType::kClearSiteSettings:
case ActionType::kClearHostedAppData:
case ActionType::kReloadPages:
action_set.clear = true;
break;
}
}
return action_set;
}
#endif // !BUILDFLAG(IS_ANDROID)
} // namespace enterprise_idle