| // Copyright 2021 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 "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/public/cpp/arc_resize_lock_type.h" |
| #include "ash/public/cpp/toast_data.h" |
| #include "ash/public/cpp/toast_manager.h" |
| #include "ash/public/cpp/window_properties.h" |
| #include "base/callback_forward.h" |
| #include "base/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/base/l10n/l10n_util.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 { |
| |
| constexpr gfx::Size kPortraitPhoneDp(412, 732); |
| constexpr gfx::Size kLandscapeTabletDp(1064, 600); |
| constexpr int kDisplayEdgeOffsetDp = 32; |
| |
| using ResizeCallback = base::OnceCallback<void(views::Widget*)>; |
| |
| gfx::Size GetPossibleSizeInWorkArea(views::Widget* widget, |
| const gfx::Size& preferred_size) { |
| auto size = gfx::SizeF(preferred_size); |
| const float preferred_aspect_ratio = size.width() / size.height(); |
| |
| auto workarea = widget->GetWorkAreaBoundsInScreen(); |
| |
| // Shrink workarea with the edge offset. |
| workarea.Inset(gfx::Insets(kDisplayEdgeOffsetDp)); |
| |
| // 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(widget->GetNativeWindow()); |
| // |shell_surface_base| can be null in unittests. |
| if (shell_surface_base) |
| size.SetToMax(gfx::SizeF(shell_surface_base->GetMinimumSize())); |
| |
| return gfx::ToFlooredSize(size); |
| } |
| |
| void ResizeToPhone(views::Widget* widget) { |
| if (widget->IsMaximized()) |
| widget->Restore(); |
| widget->CenterWindow(GetPossibleSizeInWorkArea(widget, kPortraitPhoneDp)); |
| |
| RecordResizeLockAction(ResizeLockActionType::ResizeToPhone); |
| } |
| |
| void ResizeToTablet(views::Widget* widget) { |
| if (widget->IsMaximized()) |
| widget->Restore(); |
| |
| // 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, |
| l10n_util::GetStringUTF16(IDS_ARC_COMPAT_MODE_DISABLE_RESIZE_LOCK_TOAST)); |
| toast_manager->Show(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->GetNativeWindow(), |
| 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); |
| } |
| |
| ResizeCompatMode PredictCurrentMode(const views::Widget* widget) { |
| return PredictCurrentMode(widget->GetNativeWindow()); |
| } |
| |
| ResizeCompatMode PredictCurrentMode(const aura::Window* window) { |
| const auto resize_lock_type = window->GetProperty(ash::kArcResizeLockTypeKey); |
| if (resize_lock_type == ash::ArcResizeLockType::NONE || |
| resize_lock_type == ash::ArcResizeLockType::RESIZE_ENABLED_TOGGLABLE) { |
| return ResizeCompatMode::kResizable; |
| } |
| |
| const int width = window->bounds().width(); |
| const int height = window->bounds().height(); |
| // We don't use the exact size here to predict tablet or phone size because |
| // the window size might be bigger than it due to the ARC app-side minimum |
| // size constraints. |
| if (width <= height) |
| return ResizeCompatMode::kPhone; |
| |
| return ResizeCompatMode::kTablet; |
| } |
| |
| bool ShouldShowSplashScreenDialog(ArcResizeLockPrefDelegate* pref_delegate) { |
| int show_count = pref_delegate->GetShowSplashScreenDialogCount(); |
| if (show_count == 0) |
| return false; |
| |
| pref_delegate->SetShowSplashScreenDialogCount(--show_count); |
| return true; |
| } |
| |
| } // namespace arc |