// Copyright 2019 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/ash/crostini/crostini_force_close_watcher.h"
#include <memory>
#include "base/functional/bind.h"
#include "base/memory/weak_ptr.h"
#include "base/task/sequenced_task_runner.h"
#include "base/time/time.h"
#include "chrome/browser/ui/views/crostini/crostini_force_close_view.h"
#include "components/exo/shell_surface_base.h"
#include "ui/views/widget/widget.h"
namespace crostini {
namespace {
constexpr base::TimeDelta kDefaultForceCloseDelay = base::Seconds(5);
}
ForceCloseWatcher::Delegate::~Delegate() = default;
void ForceCloseWatcher::Watch(std::unique_ptr<Delegate> delegate) {
new ForceCloseWatcher(std::move(delegate));
}
void ForceCloseWatcher::OnWidgetDestroying(views::Widget* widget) {
delegate_->Hide();
widget->RemoveObserver(this);
delete this;
}
void ForceCloseWatcher::OnCloseRequested() {
if (!show_dialog_timer_.has_value()) {
show_dialog_timer_ = base::ElapsedTimer();
return;
}
if (show_dialog_timer_->Elapsed() < force_close_delay_) {
return;
}
delegate_->Prompt();
}
void ForceCloseWatcher::OverrideDelayForTesting(base::TimeDelta delay) {
force_close_delay_ = delay;
}
ForceCloseWatcher::ForceCloseWatcher(std::unique_ptr<Delegate> delegate)
: delegate_(std::move(delegate)),
force_close_delay_(kDefaultForceCloseDelay) {
delegate_->GetClosableWidget()->AddObserver(this);
delegate_->Watched(this);
}
ForceCloseWatcher::~ForceCloseWatcher() {
CHECK(!IsInObserverList());
}
ShellSurfaceForceCloseDelegate::ShellSurfaceForceCloseDelegate(
exo::ShellSurfaceBase* shell_surface,
std::string app_name)
: shell_surface_(shell_surface),
app_name_(std::move(app_name)),
weak_ptr_factory_(this) {}
void ShellSurfaceForceCloseDelegate::ForceClose() {
// Post a task because the dialog needs to finish running its accept button
// handling code. If we CloseNow() here it will destroy the dialog's parent
// widget, destroying the dialog and causing a use-after-free.
// https://crbug.com/1215247
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindOnce(&ShellSurfaceForceCloseDelegate::ForceCloseNow,
weak_ptr_factory_.GetWeakPtr()));
}
void ShellSurfaceForceCloseDelegate::ForceCloseNow() {
// This must use CloseNow() and not Close() because ShellSurfaceBase widgets
// respond to Close() by asking the client app to close. In this case
// the app is not responding, so it won't respond to the Wayland protocol
// zxdg_toplevel_v6_send_close() message.
GetClosableWidget()->CloseNow();
}
ShellSurfaceForceCloseDelegate::~ShellSurfaceForceCloseDelegate() {
CHECK(!IsInObserverList());
}
views::Widget* ShellSurfaceForceCloseDelegate::GetClosableWidget() {
DCHECK(shell_surface_->GetWidget());
return shell_surface_->GetWidget();
}
void ShellSurfaceForceCloseDelegate::Watched(ForceCloseWatcher* watcher) {
// It is safe to use base::Unretained here. The watcher's liefetime is tied to
// the widget associated with this shell surface, and the widget's
// pre_close_callback_ can not be called on a deleted widget, so the watcher
// will also be alive.
shell_surface_->set_pre_close_callback(base::BindRepeating(
&ForceCloseWatcher::OnCloseRequested, base::Unretained(watcher)));
}
void ShellSurfaceForceCloseDelegate::Prompt() {
if (current_dialog_) {
Hide();
}
DCHECK(!current_dialog_);
current_dialog_ = ShowCrostiniForceCloseDialog(
app_name_, GetClosableWidget(),
base::BindOnce(&ShellSurfaceForceCloseDelegate::ForceClose,
weak_ptr_factory_.GetWeakPtr()));
current_dialog_->AddObserver(this);
}
void ShellSurfaceForceCloseDelegate::Hide() {
if (current_dialog_) {
current_dialog_->RemoveObserver(this);
current_dialog_->Close();
current_dialog_ = nullptr;
}
}
void ShellSurfaceForceCloseDelegate::OnWidgetDestroying(views::Widget* widget) {
if (current_dialog_) {
current_dialog_->RemoveObserver(this);
current_dialog_ = nullptr;
}
}
} // namespace crostini