// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ash/components/arc/compat_mode/resize_util.h"
#include <memory>
#include "ash/components/arc/compat_mode/arc_resize_lock_pref_delegate.h"
#include "ash/components/arc/compat_mode/arc_window_property_util.h"
#include "ash/components/arc/compat_mode/metrics.h"
#include "ash/components/arc/compat_mode/resize_confirmation_dialog_view.h"
#include "ash/constants/notifier_catalogs.h"
#include "ash/frame/non_client_frame_view_ash.h"
#include "ash/public/cpp/arc_compat_mode_util.h"
#include "ash/public/cpp/arc_resize_lock_type.h"
#include "ash/public/cpp/system/toast_data.h"
#include "ash/public/cpp/system/toast_manager.h"
#include "ash/public/cpp/window_properties.h"
#include "base/functional/callback_forward.h"
#include "base/functional/callback_helpers.h"
#include "base/stl_util.h"
#include "components/exo/shell_surface_base.h"
#include "components/exo/shell_surface_util.h"
#include "components/strings/grit/components_strings.h"
#include "ui/aura/client/aura_constants.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/ui_base_types.h"
#include "ui/display/screen.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/size_conversions.h"
#include "ui/gfx/geometry/size_f.h"
#include "ui/views/widget/widget.h"
namespace arc {
namespace {
// The following values must be the same with ARC-side hard-coded values.
// Also usually you should not directly refer to `kPortraitPhoneDp` as it may be
// adjusted on small displays (See `GetPossibleSizeInWorkArea`).
constexpr gfx::Size kPortraitPhoneDp(412, 732);
constexpr gfx::Size kLandscapeTabletDp(1064, 600);
constexpr int kDisplayEdgeOffsetDp = 27;
using ResizeCallback = base::OnceCallback<void(views::Widget*)>;
// The algorithm in `GetPossibleSizeInWorkArea` must be aligned with ARC-side.
gfx::Size GetPossibleSizeInWorkArea(aura::Window* window,
const gfx::Size& preferred_size) {
auto size = gfx::SizeF(preferred_size);
const float preferred_aspect_ratio = size.width() / size.height();
auto workarea =
display::Screen::GetScreen()->GetDisplayNearestWindow(window).work_area();
// Shrink workarea with the edge offset.
workarea.Inset(gfx::Insets(kDisplayEdgeOffsetDp));
auto* const frame_view = ash::NonClientFrameViewAsh::Get(window);
if (frame_view) {
workarea.Inset(
gfx::Insets().set_top(frame_view->NonClientTopBorderHeight()));
}
// Limit |size| to |workarea| but keep the aspect ratio.
if (size.width() > workarea.width()) {
size.set_width(workarea.width());
size.set_height(workarea.width() / preferred_aspect_ratio);
}
if (size.height() > workarea.height()) {
size.set_width(workarea.height() * preferred_aspect_ratio);
size.set_height(workarea.height());
}
const auto* shell_surface_base = exo::GetShellSurfaceBaseForWindow(window);
// |shell_surface_base| can be null in unittests.
if (shell_surface_base)
size.SetToMax(gfx::SizeF(shell_surface_base->GetMinimumSize()));
return gfx::ToFlooredSize(size);
}
gfx::Size GetPossibleSizeInWorkArea(const views::Widget* widget,
const gfx::Size& preferred_size) {
return GetPossibleSizeInWorkArea(widget->GetNativeWindow(), preferred_size);
}
void ResizeToPhone(views::Widget* widget) {
// Clear the restore state/bounds key to make sure it's going to be restored
// to normal state.
widget->GetNativeWindow()->ClearProperty(aura::client::kRestoreShowStateKey);
widget->GetNativeWindow()->ClearProperty(aura::client::kRestoreBoundsKey);
// Always make sure the window is in normal state because the window might be
// maximized/snapped.
widget->GetNativeWindow()->SetProperty(aura::client::kShowStateKey,
ui::SHOW_STATE_NORMAL);
widget->CenterWindow(GetPossibleSizeInWorkArea(widget, kPortraitPhoneDp));
RecordResizeLockAction(ResizeLockActionType::ResizeToPhone);
}
void ResizeToTablet(views::Widget* widget) {
// Clear the restore state/bounds key to make sure it's going to be restored
// to normal state.
widget->GetNativeWindow()->ClearProperty(aura::client::kRestoreShowStateKey);
widget->GetNativeWindow()->ClearProperty(aura::client::kRestoreBoundsKey);
// Always make sure the window is in normal state because the window might be
// maximized/snapped.
widget->GetNativeWindow()->SetProperty(aura::client::kShowStateKey,
ui::SHOW_STATE_NORMAL);
// We here don't shrink the preferred size according to the available workarea
// bounds like ResizeToPhone, because we'd like to let Android decide if the
// ResizeToTablet operation fallbacks to the window state change operation.
widget->CenterWindow(kLandscapeTabletDp);
RecordResizeLockAction(ResizeLockActionType::ResizeToTablet);
}
void TurnOnResizeLock(views::Widget* widget,
ArcResizeLockPrefDelegate* pref_delegate) {
const auto app_id = GetAppId(widget);
if (app_id && pref_delegate->GetResizeLockState(*app_id) !=
mojom::ArcResizeLockState::ON) {
pref_delegate->SetResizeLockState(*app_id, mojom::ArcResizeLockState::ON);
RecordResizeLockAction(ResizeLockActionType::TurnOnResizeLock);
}
}
void TurnOffResizeLock(views::Widget* target_widget,
ArcResizeLockPrefDelegate* pref_delegate) {
const auto app_id = GetAppId(target_widget);
if (!app_id || pref_delegate->GetResizeLockState(*app_id) ==
mojom::ArcResizeLockState::OFF) {
return;
}
pref_delegate->SetResizeLockState(*app_id, mojom::ArcResizeLockState::OFF);
RecordResizeLockAction(ResizeLockActionType::TurnOffResizeLock);
auto* const toast_manager = ash::ToastManager::Get();
// |toast_manager| can be null in some unittests.
if (!toast_manager)
return;
constexpr char kTurnOffResizeLockToastId[] =
"arc.compat_mode.turn_off_resize_lock";
toast_manager->Cancel(kTurnOffResizeLockToastId);
ash::ToastData toast(
kTurnOffResizeLockToastId, ash::ToastCatalogName::kAppResizable,
l10n_util::GetStringUTF16(IDS_ARC_COMPAT_MODE_DISABLE_RESIZE_LOCK_TOAST));
toast_manager->Show(std::move(toast));
}
void TurnOffResizeLockWithConfirmationIfNeeded(
views::Widget* target_widget,
ArcResizeLockPrefDelegate* pref_delegate) {
const auto app_id = GetAppId(target_widget);
if (app_id && !pref_delegate->GetResizeLockNeedsConfirmation(*app_id)) {
// The user has already agreed not to show the dialog again.
TurnOffResizeLock(target_widget, pref_delegate);
return;
}
// Set target app window as parent so that the dialog will be destroyed
// together when the app window is destroyed (e.g. app crashed).
ResizeConfirmationDialogView::Show(
/*parent=*/target_widget,
base::BindOnce(
[](views::Widget* widget, ArcResizeLockPrefDelegate* delegate,
bool accepted, bool do_not_ask_again) {
if (accepted) {
const auto app_id = GetAppId(widget);
if (do_not_ask_again && app_id)
delegate->SetResizeLockNeedsConfirmation(*app_id, false);
TurnOffResizeLock(widget, delegate);
}
},
base::Unretained(target_widget), base::Unretained(pref_delegate)));
}
} // namespace
void ResizeLockToPhone(views::Widget* widget,
ArcResizeLockPrefDelegate* pref_delegate) {
ResizeToPhone(widget);
TurnOnResizeLock(widget, pref_delegate);
}
void ResizeLockToTablet(views::Widget* widget,
ArcResizeLockPrefDelegate* pref_delegate) {
ResizeToTablet(widget);
TurnOnResizeLock(widget, pref_delegate);
}
void EnableResizingWithConfirmationIfNeeded(
views::Widget* widget,
ArcResizeLockPrefDelegate* pref_delegate) {
TurnOffResizeLockWithConfirmationIfNeeded(widget, pref_delegate);
}
bool ShouldShowSplashScreenDialog(ArcResizeLockPrefDelegate* pref_delegate) {
int show_count = pref_delegate->GetShowSplashScreenDialogCount();
if (show_count == 0)
return false;
pref_delegate->SetShowSplashScreenDialogCount(--show_count);
return true;
}
int GetUnresizableSnappedWidth(aura::Window* window) {
const auto& bounds = window->bounds();
const bool isPortrait = bounds.width() <= bounds.height();
const bool isNormal =
window->GetProperty(aura::client::kShowStateKey) == ui::SHOW_STATE_NORMAL;
if (isPortrait && isNormal) {
return bounds.width();
}
return kPortraitPhoneDp.width();
}
} // namespace arc