chromium/components/exo/client_controlled_shell_surface.cc

// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "components/exo/client_controlled_shell_surface.h"

#include <map>
#include <utility>

#include "ash/constants/ash_features.h"
#include "ash/frame/non_client_frame_view_ash.h"
#include "ash/frame/wide_frame_view.h"
#include "ash/public/cpp/arc_resize_lock_type.h"
#include "ash/public/cpp/ash_constants.h"
#include "ash/public/cpp/rounded_corner_utils.h"
#include "ash/public/cpp/shell_window_ids.h"
#include "ash/public/cpp/window_backdrop.h"
#include "ash/public/cpp/window_properties.h"
#include "ash/root_window_controller.h"
#include "ash/screen_util.h"
#include "ash/shell.h"
#include "ash/wm/client_controlled_state.h"
#include "ash/wm/collision_detection/collision_detection_utils.h"
#include "ash/wm/drag_details.h"
#include "ash/wm/pip/pip_controller.h"
#include "ash/wm/pip/pip_positioner.h"
#include "ash/wm/splitview/split_view_controller.h"
#include "ash/wm/toplevel_window_event_handler.h"
#include "ash/wm/window_positioning_utils.h"
#include "ash/wm/window_properties.h"
#include "ash/wm/window_state.h"
#include "ash/wm/window_state_delegate.h"
#include "ash/wm/window_util.h"
#include "base/logging.h"
#include "base/memory/raw_ptr.h"
#include "base/no_destructor.h"
#include "base/notreached.h"
#include "base/numerics/safe_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/trace_event/trace_event.h"
#include "base/trace_event/traced_value.h"
#include "chromeos/ui/base/window_pin_type.h"
#include "chromeos/ui/base/window_properties.h"
#include "chromeos/ui/base/window_state_type.h"
#include "chromeos/ui/frame/caption_buttons/caption_button_model.h"
#include "chromeos/ui/frame/default_frame_header.h"
#include "chromeos/ui/frame/header_view.h"
#include "chromeos/ui/frame/immersive/immersive_fullscreen_controller.h"
#include "components/exo/shell_surface_util.h"
#include "components/exo/surface.h"
#include "ui/aura/client/aura_constants.h"
#include "ui/aura/scoped_window_event_targeting_blocker.h"
#include "ui/aura/window.h"
#include "ui/aura/window_event_dispatcher.h"
#include "ui/aura/window_observer.h"
#include "ui/aura/window_tree_host.h"
#include "ui/base/class_property.h"
#include "ui/compositor/compositor.h"
#include "ui/compositor/compositor_lock.h"
#include "ui/compositor/layer.h"
#include "ui/display/display.h"
#include "ui/display/screen.h"
#include "ui/display/tablet_state.h"
#include "ui/gfx/geometry/point.h"
#include "ui/gfx/geometry/size.h"
#include "ui/views/widget/widget.h"
#include "ui/wm/core/coordinate_conversion.h"
#include "ui/wm/core/window_util.h"

DEFINE_UI_CLASS_PROPERTY_TYPE(exo::ClientControlledShellSurface*)

namespace exo {

namespace {
using ::ash::screen_util::GetIdealBoundsForMaximizedOrFullscreenOrPinnedState;
using ::chromeos::WindowStateType;

// Client controlled specific accelerators.
const struct {
  ui::KeyboardCode keycode;
  int modifiers;
  ClientControlledAcceleratorAction action;
} kAccelerators[] = {
    {ui::VKEY_OEM_MINUS, ui::EF_CONTROL_DOWN,
     ClientControlledAcceleratorAction::ZOOM_OUT},
    {ui::VKEY_OEM_PLUS, ui::EF_CONTROL_DOWN,
     ClientControlledAcceleratorAction::ZOOM_IN},
    {ui::VKEY_0, ui::EF_CONTROL_DOWN,
     ClientControlledAcceleratorAction::ZOOM_RESET},
};

ClientControlledShellSurface::DelegateFactoryCallback& GetFactoryForTesting() {
  using CallbackType = ClientControlledShellSurface::DelegateFactoryCallback;
  static base::NoDestructor<CallbackType> factory;
  return *factory;
}

// Maximum amount of time to wait for contents that match the display's
// orientation in tablet mode.
// TODO(oshima): Looks like android is generating unnecessary frames.
// Fix it on Android side and reduce the timeout.
constexpr int kOrientationLockTimeoutMs = 2500;

Orientation SizeToOrientation(const gfx::Size& size) {
  DCHECK_NE(size.width(), size.height());
  return size.width() > size.height() ? Orientation::LANDSCAPE
                                      : Orientation::PORTRAIT;
}

// A ClientControlledStateDelegate that sends the state/bounds
// change request to exo client.
class ClientControlledStateDelegate
    : public ash::ClientControlledState::Delegate {
 public:
  explicit ClientControlledStateDelegate(
      ClientControlledShellSurface* shell_surface)
      : shell_surface_(shell_surface) {}

  ClientControlledStateDelegate(const ClientControlledStateDelegate&) = delete;
  ClientControlledStateDelegate& operator=(
      const ClientControlledStateDelegate&) = delete;

  ~ClientControlledStateDelegate() override {}

  // Overridden from ash::ClientControlledState::Delegate:
  void HandleWindowStateRequest(ash::WindowState* window_state,
                                chromeos::WindowStateType next_state) override {
    shell_surface_->OnWindowStateChangeEvent(window_state->GetStateType(),
                                             next_state);
  }
  void HandleBoundsRequest(ash::WindowState* window_state,
                           chromeos::WindowStateType requested_state,
                           const gfx::Rect& bounds_in_display,
                           int64_t display_id) override {
    shell_surface_->OnBoundsChangeEvent(
        window_state->GetStateType(), requested_state, display_id,
        bounds_in_display,
        window_state->drag_details() && shell_surface_->IsDragging()
            ? window_state->drag_details()->bounds_change
            : 0,
        /*is_adjusted_bounds=*/false);
  }

 private:
  raw_ptr<ClientControlledShellSurface> shell_surface_;
};

// A WindowStateDelegate that implements ToggleFullscreen behavior for
// client controlled window.
class ClientControlledWindowStateDelegate : public ash::WindowStateDelegate {
 public:
  explicit ClientControlledWindowStateDelegate(
      ClientControlledShellSurface* shell_surface,
      ash::ClientControlledState::Delegate* delegate)
      : shell_surface_(shell_surface), delegate_(delegate) {}

  ClientControlledWindowStateDelegate(
      const ClientControlledWindowStateDelegate&) = delete;
  ClientControlledWindowStateDelegate& operator=(
      const ClientControlledWindowStateDelegate&) = delete;

  ~ClientControlledWindowStateDelegate() override {}

  // Overridden from ash::WindowStateDelegate:
  bool ToggleFullscreen(ash::WindowState* window_state) override {
    chromeos::WindowStateType next_state;
    aura::Window* window = window_state->window();
    switch (window_state->GetStateType()) {
      case chromeos::WindowStateType::kDefault:
      case chromeos::WindowStateType::kNormal:
        next_state = chromeos::WindowStateType::kFullscreen;
        break;
      case chromeos::WindowStateType::kMaximized:
        next_state = chromeos::WindowStateType::kFullscreen;
        break;
      case chromeos::WindowStateType::kFullscreen:
        switch (window->GetProperty(aura::client::kRestoreShowStateKey)) {
          case ui::SHOW_STATE_DEFAULT:
          case ui::SHOW_STATE_NORMAL:
            next_state = chromeos::WindowStateType::kNormal;
            break;
          case ui::SHOW_STATE_MAXIMIZED:
            next_state = chromeos::WindowStateType::kMaximized;
            break;
          case ui::SHOW_STATE_MINIMIZED:
            next_state = chromeos::WindowStateType::kMinimized;
            break;
          case ui::SHOW_STATE_FULLSCREEN:
          case ui::SHOW_STATE_INACTIVE:
          case ui::SHOW_STATE_END:
            DUMP_WILL_BE_NOTREACHED()
                << " unknown state :"
                << window->GetProperty(aura::client::kRestoreShowStateKey);
            return false;
        }
        break;
      case chromeos::WindowStateType::kMinimized: {
        next_state = chromeos::WindowStateType::kFullscreen;
        break;
      }
      default:
        // TODO(oshima|xdai): Handle SNAP state.
        return false;
    }
    delegate_->HandleWindowStateRequest(window_state, next_state);
    return true;
  }

  void ToggleLockedFullscreen(ash::WindowState*) override {
    // No special handling for locked ARC windows.
    return;
  }

  std::unique_ptr<ash::PresentationTimeRecorder> OnDragStarted(
      int component) override {
    shell_surface_->OnDragStarted(component);
    return nullptr;
  }

  void OnDragFinished(bool canceled, const gfx::PointF& location) override {
    shell_surface_->OnDragFinished(canceled, location);
  }

 private:
  raw_ptr<ClientControlledShellSurface> shell_surface_;
  raw_ptr<ash::ClientControlledState::Delegate, DanglingUntriaged> delegate_;
};

bool IsPinned(const ash::WindowState* window_state) {
  return window_state->IsPinned() || window_state->IsTrustedPinned();
}

class CaptionButtonModel : public chromeos::CaptionButtonModel {
 public:
  CaptionButtonModel(uint32_t visible_button_mask, uint32_t enabled_button_mask)
      : visible_button_mask_(visible_button_mask),
        enabled_button_mask_(enabled_button_mask) {}

  CaptionButtonModel(const CaptionButtonModel&) = delete;
  CaptionButtonModel& operator=(const CaptionButtonModel&) = delete;

  // Overridden from ash::CaptionButtonModel:
  bool IsVisible(views::CaptionButtonIcon icon) const override {
    return visible_button_mask_ & (1 << icon);
  }
  bool IsEnabled(views::CaptionButtonIcon icon) const override {
    return enabled_button_mask_ & (1 << icon);
  }
  bool InZoomMode() const override {
    return visible_button_mask_ & (1 << views::CAPTION_BUTTON_ICON_ZOOM);
  }

 private:
  uint32_t visible_button_mask_;
  uint32_t enabled_button_mask_;
};

// EventTargetingBlocker blocks the event targeting by setting NONE targeting
// policy to the window subtrees. It resets to the original policy upon
// deletion.
class EventTargetingBlocker : aura::WindowObserver {
 public:
  EventTargetingBlocker() = default;

  EventTargetingBlocker(const EventTargetingBlocker&) = delete;
  EventTargetingBlocker& operator=(const EventTargetingBlocker&) = delete;

  ~EventTargetingBlocker() override {
    if (window_)
      Unregister(window_);
  }

  void Block(aura::Window* window) {
    window_ = window;
    Register(window);
  }

 private:
  void Register(aura::Window* window) {
    window->AddObserver(this);
    event_targeting_blocker_map_[window] =
        std::make_unique<aura::ScopedWindowEventTargetingBlocker>(window);
    for (aura::Window* child : window->children()) {
      Register(child);
    }
  }

  void Unregister(aura::Window* window) {
    window->RemoveObserver(this);
    event_targeting_blocker_map_.erase(window);
    for (aura::Window* child : window->children()) {
      Unregister(child);
    }
  }

  void OnWindowDestroying(aura::Window* window) override {
    Unregister(window);
    if (window_ == window)
      window_ = nullptr;
  }

  std::map<aura::Window*,
           std::unique_ptr<aura::ScopedWindowEventTargetingBlocker>>
      event_targeting_blocker_map_;
  raw_ptr<aura::Window> window_ = nullptr;
};

}  // namespace

class ClientControlledShellSurface::ScopedSetBoundsLocally {
 public:
  explicit ScopedSetBoundsLocally(ClientControlledShellSurface* shell_surface)
      : state_(shell_surface->client_controlled_state_) {
    state_->set_bounds_locally(true);
  }

  ScopedSetBoundsLocally(const ScopedSetBoundsLocally&) = delete;
  ScopedSetBoundsLocally& operator=(const ScopedSetBoundsLocally&) = delete;

  ~ScopedSetBoundsLocally() { state_->set_bounds_locally(false); }

 private:
  const raw_ptr<ash::ClientControlledState> state_;
};

class ClientControlledShellSurface::ScopedLockedToRoot {
 public:
  explicit ScopedLockedToRoot(views::Widget* widget)
      : window_(widget->GetNativeWindow()) {
    window_->SetProperty(ash::kLockedToRootKey, true);
  }

  ScopedLockedToRoot(const ScopedLockedToRoot&) = delete;
  ScopedLockedToRoot& operator=(const ScopedLockedToRoot&) = delete;

  ~ScopedLockedToRoot() { window_->ClearProperty(ash::kLockedToRootKey); }

 private:
  const raw_ptr<aura::Window> window_;
};

class ClientControlledShellSurface::ScopedDeferWindowStateUpdate {
 public:
  explicit ScopedDeferWindowStateUpdate(
      ClientControlledShellSurface* shell_surface)
      : shell_surface_(shell_surface) {
    CHECK(!shell_surface_->scoped_defer_window_state_update_);
    shell_surface_->scoped_defer_window_state_update_ = base::WrapUnique(this);
    // Do not activate if the widget is initially minimized.
    if (shell_surface->GetWidget()->IsMinimized()) {
      can_activate_ =
          shell_surface->GetWidget()->widget_delegate()->CanActivate();
      shell_surface->GetWidget()->widget_delegate()->SetCanActivate(false);
    }
  }

  ScopedDeferWindowStateUpdate(const ScopedDeferWindowStateUpdate&) = delete;
  ScopedDeferWindowStateUpdate& operator=(const ScopedDeferWindowStateUpdate&) =
      delete;

  ~ScopedDeferWindowStateUpdate() {
    auto self = shell_surface_->scoped_defer_window_state_update_.release();
    DCHECK_EQ(self, this);
    if (can_activate_.has_value()) {
      shell_surface_->GetWidget()->widget_delegate()->SetCanActivate(
          can_activate_.value());
    }
    if (next_state_) {
      shell_surface_->OnWindowStateChangeEvent(*next_state_, *next_state_);
    }
  }

  void SetNextState(chromeos::WindowStateType next_state) {
    next_state_ = next_state;
  }

 private:
  raw_ptr<ClientControlledShellSurface> shell_surface_;
  std::optional<chromeos::WindowStateType> next_state_;
  std::optional<bool> can_activate_;
};

////////////////////////////////////////////////////////////////////////////////
// ClientControlledShellSurface, public:

ClientControlledShellSurface::ClientControlledShellSurface(
    Surface* surface,
    bool can_minimize,
    int container,
    bool default_scale_cancellation,
    bool supports_floated_state)
    : ShellSurfaceBase(surface, gfx::Point(), can_minimize, container),
      use_default_scale_cancellation_(default_scale_cancellation),
      supports_floated_state_(supports_floated_state) {
  server_side_resize_ = true;
  set_client_submits_surfaces_in_pixel_coordinates(true);
}

ClientControlledShellSurface::~ClientControlledShellSurface() {
  // Reset the window delegate here so that we won't try to do any dragging
  // operation on a to-be-destroyed window. |widget_| can be nullptr in tests.
  if (GetWidget())
    GetWindowState()->SetDelegate(nullptr);
  if (client_controlled_state_)
    client_controlled_state_->ResetDelegate();
  wide_frame_.reset();
}

void ClientControlledShellSurface::SetBounds(int64_t display_id,
                                             const gfx::Rect& bounds) {
  TRACE_EVENT2("exo", "ClientControlledShellSurface::SetBounds", "display_id",
               display_id, "bounds", bounds.ToString());

  if (bounds.IsEmpty()) {
    DLOG(WARNING) << "Bounds must be non-empty";
    return;
  }

  SetDisplay(display_id);

  const gfx::Rect bounds_dp =
      gfx::ScaleToRoundedRect(bounds, GetClientToDpPendingScale());
  SetGeometry(bounds_dp);
}

void ClientControlledShellSurface::SetBoundsOrigin(int64_t display_id,
                                                   const gfx::Point& origin) {
  TRACE_EVENT2("exo", "ClientControlledShellSurface::SetBoundsOrigin",
               "display_id", display_id, "origin", origin.ToString());
  SetDisplay(display_id);
  const gfx::Point origin_dp =
      gfx::ScaleToRoundedPoint(origin, GetClientToDpPendingScale());
  pending_geometry_.set_origin(origin_dp);
}

void ClientControlledShellSurface::SetBoundsSize(const gfx::Size& size) {
  TRACE_EVENT1("exo", "ClientControlledShellSurface::SetBoundsSize", "size",
               size.ToString());

  if (size.IsEmpty()) {
    DLOG(WARNING) << "Bounds size must be non-empty";
    return;
  }

  const gfx::Size size_dp =
      gfx::ScaleToRoundedSize(size, GetClientToDpPendingScale());
  pending_geometry_.set_size(size_dp);
}

void ClientControlledShellSurface::SetMaximized() {
  TRACE_EVENT0("exo", "ClientControlledShellSurface::SetMaximized");
  pending_window_state_ = chromeos::WindowStateType::kMaximized;
}

void ClientControlledShellSurface::SetMinimized() {
  TRACE_EVENT0("exo", "ClientControlledShellSurface::SetMinimized");
  pending_window_state_ = chromeos::WindowStateType::kMinimized;
}

void ClientControlledShellSurface::SetRestored() {
  TRACE_EVENT0("exo", "ClientControlledShellSurface::SetRestored");
  pending_window_state_ = chromeos::WindowStateType::kNormal;
}

void ClientControlledShellSurface::SetFullscreen(bool fullscreen,
                                                 int64_t display_id) {
  TRACE_EVENT1("exo", "ClientControlledShellSurface::SetFullscreen",
               "fullscreen", fullscreen);
  pending_window_state_ = fullscreen ? chromeos::WindowStateType::kFullscreen
                                     : chromeos::WindowStateType::kNormal;
  // TODO(crbug.com/40280523): `display_id` might need to be used here
  // somewhere.
}

void ClientControlledShellSurface::SetPinned(chromeos::WindowPinType type) {
  TRACE_EVENT1("exo", "ClientControlledShellSurface::SetPinned", "type",
               static_cast<int>(type));

  if (!widget_)
    CreateShellSurfaceWidget(ui::SHOW_STATE_NORMAL);

  if (type == chromeos::WindowPinType::kNone) {
    // Set other window state mode will automatically cancelled pin mode.
    // TODO: Add NOTREACH() here after ARC side integration fully landed.
  } else {
    bool trusted = type == chromeos::WindowPinType::kTrustedPinned;
    pending_window_state_ = trusted ? chromeos::WindowStateType::kTrustedPinned
                                    : chromeos::WindowStateType::kPinned;
  }
}

void ClientControlledShellSurface::SetSystemUiVisibility(bool autohide) {
  TRACE_EVENT1("exo", "ClientControlledShellSurface::SetSystemUiVisibility",
               "autohide", autohide);

  if (!widget_)
    CreateShellSurfaceWidget(ui::SHOW_STATE_NORMAL);

  ash::window_util::SetAutoHideShelf(widget_->GetNativeWindow(), autohide);
}

void ClientControlledShellSurface::SetAlwaysOnTop(bool always_on_top) {
  TRACE_EVENT1("exo", "ClientControlledShellSurface::SetAlwaysOnTop",
               "always_on_top", always_on_top);
  pending_always_on_top_ = always_on_top;
}

void ClientControlledShellSurface::SetOrientation(Orientation orientation) {
  TRACE_EVENT1("exo", "ClientControlledShellSurface::SetOrientation",
               "orientation",
               orientation == Orientation::PORTRAIT ? "portrait" : "landscape");
  pending_orientation_ = orientation;
}

void ClientControlledShellSurface::SetShadowBounds(const gfx::Rect& bounds) {
  TRACE_EVENT1("exo", "ClientControlledShellSurface::SetShadowBounds", "bounds",
               bounds.ToString());
  auto shadow_bounds =
      bounds.IsEmpty() ? std::nullopt : std::make_optional(bounds);
  if (shadow_bounds_ != shadow_bounds) {
    shadow_bounds_ = shadow_bounds;
    shadow_bounds_changed_ = true;
  }
}

void ClientControlledShellSurface::OnWindowStateChangeEvent(
    chromeos::WindowStateType current_state,
    chromeos::WindowStateType next_state) {
  // Android already knows this state change. Don't send state change to Android
  // that it is about to do anyway.
  if (scoped_defer_window_state_update_) {
    scoped_defer_window_state_update_->SetNextState(next_state);
    return;
  }

  if (delegate_ && pending_window_state_ != next_state)
    delegate_->OnStateChanged(current_state, next_state);
}

void ClientControlledShellSurface::StartDrag(int component,
                                             const gfx::PointF& location) {
  TRACE_EVENT2("exo", "ClientControlledShellSurface::StartDrag", "component",
               component, "location", location.ToString());

  if (!widget_)
    return;
  AttemptToStartDrag(component, location);
}

void ClientControlledShellSurface::AttemptToStartDrag(
    int component,
    const gfx::PointF& location) {
  aura::Window* target = widget_->GetNativeWindow();
  ash::ToplevelWindowEventHandler* toplevel_handler =
      ash::Shell::Get()->toplevel_window_event_handler();
  aura::Window* mouse_pressed_handler =
      target->GetHost()->dispatcher()->mouse_pressed_handler();
  // Start dragging only if:
  // 1) touch guesture is in progress or
  // 2) mouse was pressed on the target or its subsurfaces.
  // If neither condition is met, we do not start the drag.
  gfx::PointF point_in_root;
  if (toplevel_handler->gesture_target()) {
    point_in_root = toplevel_handler->event_location_in_gesture_target();
    aura::Window::ConvertPointToTarget(
        toplevel_handler->gesture_target(),
        widget_->GetNativeWindow()->GetRootWindow(), &point_in_root);
  } else if (mouse_pressed_handler && target->Contains(mouse_pressed_handler)) {
    point_in_root = location;
    if (use_default_scale_cancellation_) {
      // When default scale cancellation is enabled, the client sends the
      // location in screen coordinates. Otherwise, the location should already
      // be in the display's coordinates.
      wm::ConvertPointFromScreen(target->GetRootWindow(), &point_in_root);
    }
  } else {
    return;
  }
  toplevel_handler->AttemptToStartDrag(
      target, point_in_root, component,
      ash::ToplevelWindowEventHandler::EndClosure());
}

bool ClientControlledShellSurface::IsDragging() {
  return in_drag_;
}

void ClientControlledShellSurface::SetCanMaximize(bool can_maximize) {
  TRACE_EVENT1("exo", "ClientControlledShellSurface::SetCanMaximize",
               "can_maximzie", can_maximize);
  can_maximize_ = can_maximize;
  if (widget_)
    widget_->OnSizeConstraintsChanged();
}

void ClientControlledShellSurface::UpdateAutoHideFrame() {
  if (immersive_fullscreen_controller_) {
    bool enabled = (frame_type_ == SurfaceFrameType::AUTOHIDE &&
                    (GetWindowState()->IsMaximizedOrFullscreenOrPinned() ||
                     GetWindowState()->IsSnapped()));
    chromeos::ImmersiveFullscreenController::EnableForWidget(widget_, enabled);
  }
}

void ClientControlledShellSurface::SetFrameButtons(
    uint32_t visible_button_mask,
    uint32_t enabled_button_mask) {
  if (frame_visible_button_mask_ == visible_button_mask &&
      frame_enabled_button_mask_ == enabled_button_mask) {
    return;
  }
  frame_visible_button_mask_ = visible_button_mask;
  frame_enabled_button_mask_ = enabled_button_mask;

  if (widget_)
    UpdateCaptionButtonModel();
}

void ClientControlledShellSurface::SetExtraTitle(
    const std::u16string& extra_title) {
  TRACE_EVENT1("exo", "ClientControlledShellSurface::SetExtraTitle",
               "extra_title", base::UTF16ToUTF8(extra_title));

  if (!widget_) {
    initial_extra_title_ = extra_title;
    return;
  }

  GetFrameView()->GetHeaderView()->GetFrameHeader()->SetFrameTextOverride(
      extra_title);
  if (wide_frame_) {
    wide_frame_->header_view()->GetFrameHeader()->SetFrameTextOverride(
        extra_title);
  }
}

void ClientControlledShellSurface::RebindRootSurface(
    Surface* root_surface,
    bool can_minimize,
    int container,
    bool default_scale_cancellation,
    bool supports_floated_state) {
  use_default_scale_cancellation_ = default_scale_cancellation;
  supports_floated_state_ = supports_floated_state;
  auto* const window = widget_ ? widget_->GetNativeWindow() : nullptr;
  if (window) {
    window->SetProperty(chromeos::kSupportsFloatedStateKey,
                        supports_floated_state_);
  }
  ShellSurfaceBase::RebindRootSurface(root_surface, can_minimize, container);
}

void ClientControlledShellSurface::DidReceiveCompositorFrameAck() {
  orientation_ = pending_orientation_;
  // Unlock the compositor after the frame is received by viz so that
  // screenshot contain the correct frame.
  if (expected_orientation_ == orientation_)
    orientation_compositor_lock_.reset();
  SurfaceTreeHost::DidReceiveCompositorFrameAck();
}

void ClientControlledShellSurface::OnBoundsChangeEvent(
    chromeos::WindowStateType current_state,
    chromeos::WindowStateType requested_state,
    int64_t display_id,
    const gfx::Rect& window_bounds,
    int bounds_change,
    bool is_adjusted_bounds) {
  // 1) Do no update the bounds unless we have geometry from client.
  // 2) Do not update the bounds if window is minimized unless it
  // exiting the minimzied state.
  // The bounds will be provided by client when unminimized.
  if (geometry().IsEmpty() || window_bounds.IsEmpty() ||
      (widget_->IsMinimized() &&
       requested_state == chromeos::WindowStateType::kMinimized) ||
      !delegate_) {
    return;
  }

  // Sends the client bounds, which matches the geometry
  // when frame is enabled.
  const gfx::Rect client_bounds = GetClientBoundsForWindowBoundsAndWindowState(
      window_bounds, requested_state);

  gfx::Size current_size = GetFrameView()->GetBoundsForClientView().size();
  bool is_resize = client_bounds.size() != current_size &&
                   !widget_->IsMaximized() && !widget_->IsFullscreen();

  // Make sure to use the up-to-date scale factor.
  display::Display display;
  const bool display_exists =
      display::Screen::GetScreen()->GetDisplayWithDisplayId(display_id,
                                                            &display);
  DCHECK(display_exists && display.is_valid());
  const float scale =
      use_default_scale_cancellation_ ? 1.f : display.device_scale_factor();
  const gfx::Rect scaled_client_bounds =
      gfx::ScaleToRoundedRect(client_bounds, scale);
  delegate_->OnBoundsChanged(current_state, requested_state, display_id,
                             scaled_client_bounds, is_resize, bounds_change,
                             is_adjusted_bounds);

  auto* window_state = GetWindowState();
  if (server_reparent_window_ &&
      window_state->GetDisplay().id() != display_id) {
    ScopedSetBoundsLocally scoped_set_bounds(this);
    int container_id = window_state->window()->parent()->GetId();
    aura::Window* new_parent =
        ash::Shell::GetRootWindowControllerWithDisplayId(display_id)
            ->GetContainer(container_id);
    new_parent->AddChild(window_state->window());
  }
}

void ClientControlledShellSurface::ChangeZoomLevel(ZoomChange change) {
  if (delegate_)
    delegate_->OnZoomLevelChanged(change);
}

void ClientControlledShellSurface::OnDragStarted(int component) {
  in_drag_ = true;
  if (delegate_)
    delegate_->OnDragStarted(component);
}

void ClientControlledShellSurface::OnDragFinished(bool canceled,
                                                  const gfx::PointF& location) {
  in_drag_ = false;
  if (!delegate_)
    return;

  const float scale = 1.f / GetClientToDpScale();
  const gfx::PointF scaled = gfx::ScalePoint(location, scale);
  delegate_->OnDragFinished(scaled.x(), scaled.y(), canceled);
}

float ClientControlledShellSurface::GetClientToDpScale() const {
  // If the default_device_scale_factor is used for scale cancellation,
  // we expect the client will already send bounds in DP.
  if (use_default_scale_cancellation_)
    return 1.f;
  return 1.f / GetScale();
}

void ClientControlledShellSurface::SetResizeLockType(
    ash::ArcResizeLockType resize_lock_type) {
  TRACE_EVENT1("exo", "ClientControlledShellSurface::SetResizeLockType",
               "resize_lock_type", resize_lock_type);
  pending_resize_lock_type_ = resize_lock_type;
}

void ClientControlledShellSurface::UpdateResizability() {
  TRACE_EVENT0("exo", "ClientControlledShellSurface::updateCanResize");
  widget_->GetNativeWindow()->SetProperty(ash::kArcResizeLockTypeKey,
                                          pending_resize_lock_type_);
  // If resize lock is enabled, the window is explicitly marded as unresizable.
  // Otherwise, the decision is deferred to the parent class.
  if (pending_resize_lock_type_ ==
           ash::ArcResizeLockType::RESIZE_DISABLED_TOGGLABLE ||
       pending_resize_lock_type_ ==
           ash::ArcResizeLockType::RESIZE_DISABLED_NONTOGGLABLE) {
    SetCanResize(false);
    return;
  }
  ShellSurfaceBase::UpdateResizability();
}

////////////////////////////////////////////////////////////////////////////////
// SurfaceDelegate overrides:

bool ClientControlledShellSurface::IsInputEnabled(Surface* surface) const {
  // Client-driven dragging/resizing relies on implicit grab, which ensures that
  // mouse/touch events are delivered to the focused surface until release, even
  // if they fall outside surface bounds. However, if the client destroys the
  // surface with implicit grab, the drag/resize is prematurely ended. Prevent
  // this by delivering all input events to the root surface, which shares the
  // lifetime of the shell surface.
  // TODO(domlaskowski): Remove once the client is provided with an API to hook
  // into server-driven dragging/resizing.
  return surface == root_surface();
}

void ClientControlledShellSurface::OnSetFrame(SurfaceFrameType type) {
  pending_frame_type_ = type;
}

void ClientControlledShellSurface::OnSetFrameColors(SkColor active_color,
                                                    SkColor inactive_color) {
  ShellSurfaceBase::OnSetFrameColors(active_color, inactive_color);
  if (wide_frame_) {
    aura::Window* window = wide_frame_->GetWidget()->GetNativeWindow();
    window->SetProperty(chromeos::kTrackDefaultFrameColors, false);
    window->SetProperty(chromeos::kFrameActiveColorKey, active_color);
    window->SetProperty(chromeos::kFrameInactiveColorKey, inactive_color);
  }
}

void ClientControlledShellSurface::SetSnapPrimary(float snap_ratio) {
  TRACE_EVENT0("exo", "ClientControlledShellSurface::SetSnappedToPrimary");
  pending_window_state_ = chromeos::WindowStateType::kPrimarySnapped;
}

void ClientControlledShellSurface::SetSnapSecondary(float snap_ratio) {
  TRACE_EVENT0("exo", "ClientControlledShellSurface::SetSnappedToSecondary");
  pending_window_state_ = chromeos::WindowStateType::kSecondarySnapped;
}

void ClientControlledShellSurface::SetPip() {
  TRACE_EVENT0("exo", "ClientControlledShellSurface::SetPip");
  pending_window_state_ = chromeos::WindowStateType::kPip;
}

void ClientControlledShellSurface::UnsetPip() {
  TRACE_EVENT0("exo", "ClientControlledShellSurface::UnsetPip");
  SetRestored();
}

void ClientControlledShellSurface::SetFloatToLocation(
    chromeos::FloatStartLocation float_start_location) {
  TRACE_EVENT0("exo", "ClientControlledShellSurface::SetFloatToLocation");
  pending_window_state_ = chromeos::WindowStateType::kFloated;
}

void ClientControlledShellSurface::OnDidProcessDisplayChanges(
    const DisplayConfigurationChange& configuration_change) {
  ShellSurfaceBase::OnDidProcessDisplayChanges(configuration_change);

  if (!widget_) {
    return;
  }

  // The PIP window bounds is adjusted in Ash when the screen is rotated, but
  // Android has an obsolete bounds for a while and applies it incorrectly.
  // We need to ignore those bounds change until the states are completely
  // synced on both sides.
  const bool any_displays_rotated = base::ranges::any_of(
      configuration_change.display_metrics_changes,
      [](const DisplayManagerObserver::DisplayMetricsChange& change) {
        return change.changed_metrics &
               display::DisplayObserver::DISPLAY_METRIC_ROTATION;
      });
  if (GetWindowState()->IsPip() && any_displays_rotated) {
    gfx::Rect bounds_after_rotation =
        ash::PipPositioner::GetSnapFractionAppliedBounds(GetWindowState());
    display_rotating_with_pip_ =
        bounds_after_rotation !=
        GetWindowState()->window()->GetBoundsInScreen();
  }

  // Early return if no display changes are relevant to the shell surface's host
  // display.
  const auto host_display_change = base::ranges::find(
      configuration_change.display_metrics_changes, output_display_id(),
      [](const DisplayManagerObserver::DisplayMetricsChange& change) {
        return change.display->id();
      });
  if (host_display_change ==
      configuration_change.display_metrics_changes.end()) {
    return;
  }

  uint32_t changed_metrics = host_display_change->changed_metrics;
  if (!display::Screen::GetScreen()->InTabletMode() || !widget_->IsActive() ||
      !(changed_metrics & display::DisplayObserver::DISPLAY_METRIC_ROTATION)) {
    return;
  }

  Orientation target_orientation =
      SizeToOrientation(host_display_change->display->size());
  if (orientation_ == target_orientation) {
    return;
  }
  expected_orientation_ = target_orientation;
  EnsureCompositorIsLockedForOrientationChange();
}

////////////////////////////////////////////////////////////////////////////////
// aura::WindowObserver overrides:
void ClientControlledShellSurface::OnWindowDestroying(aura::Window* window) {
  if (client_controlled_state_) {
    client_controlled_state_->ResetDelegate();
    client_controlled_state_ = nullptr;
  }
  ShellSurfaceBase::OnWindowDestroying(window);
}

void ClientControlledShellSurface::OnWindowAddedToRootWindow(
    aura::Window* window) {
  // Window dragging across display moves the window to target display when
  // dropped, but the actual window bounds comes later from android.  Update the
  // window bounds now so that the window stays where it is expected to be. (it
  // may still move if the android sends different bounds).
  if (client_controlled_state_->set_bounds_locally() ||
      !GetWindowState()->is_dragged()) {
    return;
  }

  ScopedLockedToRoot scoped_locked_to_root(widget_);
  UpdateWidgetBounds();
}

////////////////////////////////////////////////////////////////////////////////
// views::WidgetDelegate overrides:

void ClientControlledShellSurface::WindowClosing() {
  wide_frame_.reset();
  ShellSurfaceBase::WindowClosing();
}

bool ClientControlledShellSurface::CanMaximize() const {
  return can_maximize_;
}

std::unique_ptr<views::NonClientFrameView>
ClientControlledShellSurface::CreateNonClientFrameView(views::Widget* widget) {
  ash::WindowState* window_state = GetWindowState();
  std::unique_ptr<ash::ClientControlledState::Delegate> delegate =
      GetFactoryForTesting()
          ? GetFactoryForTesting().Run()
          : std::make_unique<ClientControlledStateDelegate>(this);

  auto window_delegate = std::make_unique<ClientControlledWindowStateDelegate>(
      this, delegate.get());
  auto state =
      std::make_unique<ash::ClientControlledState>(std::move(delegate));
  client_controlled_state_ = state.get();
  window_state->SetStateObject(std::move(state));
  window_state->SetDelegate(std::move(window_delegate));
  auto frame_view = CreateNonClientFrameViewInternal(widget);
  immersive_fullscreen_controller_ =
      std::make_unique<chromeos::ImmersiveFullscreenController>();
  static_cast<ash::NonClientFrameViewAsh*>(frame_view.get())
      ->InitImmersiveFullscreenControllerForView(
          immersive_fullscreen_controller_.get());
  return frame_view;
}

bool ClientControlledShellSurface::ShouldSaveWindowPlacement() const {
  return false;
}

void ClientControlledShellSurface::SaveWindowPlacement(
    const gfx::Rect& bounds,
    ui::WindowShowState show_state) {}

bool ClientControlledShellSurface::GetSavedWindowPlacement(
    const views::Widget* widget,
    gfx::Rect* bounds,
    ui::WindowShowState* show_state) const {
  return false;
}

////////////////////////////////////////////////////////////////////////////////
// views::View overrides:

gfx::Size ClientControlledShellSurface::GetMaximumSize() const {
  if (can_maximize_) {
    // On ChromeOS, a window with non empty maximum size is non-maximizable,
    // even if CanMaximize() returns true. ClientControlledShellSurface
    // sololy depends on |can_maximize_| to determine if it is maximizable,
    // so just return empty size.
    return gfx::Size();
  } else {
    return ShellSurfaceBase::GetMaximumSize();
  }
}

void ClientControlledShellSurface::OnDeviceScaleFactorChanged(float old_dsf,
                                                              float new_dsf) {
  views::View::OnDeviceScaleFactorChanged(old_dsf, new_dsf);

  UpdateFrameWidth();
}

////////////////////////////////////////////////////////////////////////////////
// ui::CompositorLockClient overrides:

void ClientControlledShellSurface::CompositorLockTimedOut() {
  orientation_compositor_lock_.reset();
}

////////////////////////////////////////////////////////////////////////////////
// ShellSurfaceBase overrides:

void ClientControlledShellSurface::SetSystemModal(bool system_modal) {
  // System modal container is used by clients to implement client side
  // managed system modal dialogs using a single ShellSurface instance.
  // Hit-test region will be non-empty when at least one dialog exists on
  // the client side. Here we detect the transition between no client side
  // dialog and at least one dialog so activatable state is properly
  // updated.
  if (container_ != ash::kShellWindowId_SystemModalContainer) {
    LOG(ERROR)
        << "Only a window in SystemModalContainer can change the modality";
    return;
  }

  ShellSurfaceBase::SetSystemModal(system_modal);
}

void ClientControlledShellSurface::SetWidgetBounds(const gfx::Rect& bounds,
                                                   bool adjusted_by_server) {
  set_bounds_is_dirty(true);
  const auto* screen = display::Screen::GetScreen();
  aura::Window* window = widget_->GetNativeWindow();
  display::Display current_display = screen->GetDisplayNearestWindow(window);

  bool is_display_move_pending = false;
  display::Display target_display = current_display;

  display::Display display;
  if (screen->GetDisplayWithDisplayId(display_id_, &display)) {
    bool is_display_stale = display_id_ != current_display.id();

    // Preserve widget bounds until client acknowledges display move.
    if (preserve_widget_bounds_ && is_display_stale)
      return;

    // True if the window has just been reparented to another root window, and
    // the move was initiated by the server.
    // TODO(oshima): Improve the window moving logic. https://crbug.com/875047
    is_display_move_pending =
        window->GetProperty(ash::kLockedToRootKey) && is_display_stale;

    if (!is_display_move_pending)
      target_display = display;

    preserve_widget_bounds_ = is_display_move_pending;
  } else {
    preserve_widget_bounds_ = false;
  }

  // Calculate a minimum window visibility required bounds.
  // TODO(oshima): Move this to ComputeAdjustedBounds.
  gfx::Rect adjusted_bounds = bounds;
  if (!is_display_move_pending) {
    const gfx::Rect& restriction = GetWindowState()->IsFullscreen()
                                       ? target_display.bounds()
                                       : target_display.work_area();
    ash::AdjustBoundsToEnsureMinimumWindowVisibility(
        restriction, /*client_controlled=*/true, &adjusted_bounds);
    // Collision detection to the bounds set by Android should be applied only
    // to initial bounds and any client-requested bounds (I.E. Double-Tap to
    // resize). Do not adjust new bounds for fling/display rotation as it can be
    // obsolete or in transit during animation, which results in incorrect
    // resting postiion. The resting position should be fully controlled by
    // chrome afterwards because Android isn't aware of Chrome OS System UI.
    const bool is_resizing_without_rotation =
        !display_rotating_with_pip_ && !IsDragging() &&
        !ash::Shell::Get()->pip_controller()->is_tucked() &&
        GetWindowState()->GetCurrentBoundsInScreen().size() != bounds.size();
    if (GetWindowState()->IsPip() &&
        (!ash::PipPositioner::HasSnapFraction(GetWindowState()) ||
         is_resizing_without_rotation)) {
      adjusted_bounds = ash::CollisionDetectionUtils::GetRestingPosition(
          target_display, adjusted_bounds,
          ash::CollisionDetectionUtils::RelativePriority::kPictureInPicture);

      // Only if the window is resizing with a double tap, the bounds should
      // be applied via a scaling animation. Position changes will be applied
      // via kAnimate.
      if (is_resizing_without_rotation && !IsDragging()) {
        client_controlled_state_->set_next_bounds_change_animation_type(
            ash::WindowState::BoundsChangeAnimationType::kCrossFade);
      }
    }
  }

  if (adjusted_bounds == widget_->GetWindowBoundsInScreen() &&
      target_display.id() == current_display.id()) {
    return;
  }

  bool set_bounds_locally =
      display_rotating_with_pip_ ||
      (GetWindowState()->is_dragged() && !is_display_move_pending);

  if (set_bounds_locally || client_controlled_state_->set_bounds_locally()) {
    // Convert from screen to display coordinates.
    gfx::Point origin = bounds.origin();
    wm::ConvertPointFromScreen(window->parent(), &origin);

    // Move the window relative to the current display.
    {
      ScopedSetBoundsLocally scoped_set_bounds(this);
      window->SetBounds(gfx::Rect(origin, adjusted_bounds.size()));
    }
    UpdateHostWindowOrigin();
    return;
  }

  {
    ScopedSetBoundsLocally scoped_set_bounds(this);
    window->SetBoundsInScreen(adjusted_bounds, target_display);
  }

  if (bounds != adjusted_bounds || is_display_move_pending) {
    // Notify client that bounds were adjusted or window moved across displays.
    auto state_type = GetWindowState()->GetStateType();
    gfx::Rect adjusted_bounds_in_display(adjusted_bounds);

    adjusted_bounds_in_display.Offset(
        -target_display.bounds().OffsetFromOrigin());

    OnBoundsChangeEvent(state_type, state_type, target_display.id(),
                        adjusted_bounds_in_display, 0,
                        /*is_adjusted_bounds=*/true);
  }

  UpdateHostWindowOrigin();
}
gfx::Rect ClientControlledShellSurface::GetVisibleBounds() const {
  const auto* screen = display::Screen::GetScreen();
  display::Display display;

  if (geometry_.IsEmpty() ||
      !screen->GetDisplayWithDisplayId(display_id_, &display)) {
    return ShellSurfaceBase::GetVisibleBounds();
  }
  // ARC sends geometry_ in screen coordinates.
  return geometry_ + display.bounds().OffsetFromOrigin();
}

gfx::Rect ClientControlledShellSurface::GetShadowBounds() const {
  gfx::Rect shadow_bounds = ShellSurfaceBase::GetShadowBounds();
  const ash::NonClientFrameViewAsh* frame_view = GetFrameView();
  if (frame_view->GetFrameEnabled() && !shadow_bounds_->IsEmpty() &&
      !geometry_.IsEmpty() && !frame_view->GetFrameOverlapped()) {
    // The client controlled geometry is only for the client
    // area. When the chrome side frame is enabled, the shadow height
    // has to include the height of the frame, and the total height is
    // equals to the window height computed by
    // |GetWindowBoundsForClientBounds|.
    // But when the frame is overlapped with the client area, shadow bounds
    // should be the same as the client area bounds.
    shadow_bounds.set_size(
        frame_view->GetWindowBoundsForClientBounds(shadow_bounds).size());
  }

  return shadow_bounds;
}

void ClientControlledShellSurface::InitializeWindowState(
    ash::WindowState* window_state) {
  // Allow the client to request bounds that do not fill the entire work area
  // when maximized, or the entire display when fullscreen.
  window_state->set_allow_set_bounds_direct(true);
  window_state->set_ignore_keyboard_bounds_change(true);
  if (container_ == ash::kShellWindowId_SystemModalContainer ||
      container_ == ash::kShellWindowId_ArcVirtualKeyboardContainer) {
    DisableMovement();
  }
  ash::NonClientFrameViewAsh* frame_view = GetFrameView();
  frame_view->SetCaptionButtonModel(std::make_unique<CaptionButtonModel>(
      frame_visible_button_mask_, frame_enabled_button_mask_));
  UpdateAutoHideFrame();
  UpdateFrameWidth();
  if (initial_orientation_lock_ != chromeos::OrientationType::kAny)
    SetOrientationLock(initial_orientation_lock_);
  if (initial_extra_title_ != std::u16string())
    SetExtraTitle(initial_extra_title_);

  // Register Client controlled accelerators.
  views::FocusManager* focus_manager = widget_->GetFocusManager();
  accelerator_target_ =
      std::make_unique<ClientControlledAcceleratorTarget>(this);

  // These shortcuts are same as ones used in chrome.
  // TODO: investigate if we need to reassign.
  for (const auto& entry : kAccelerators) {
    focus_manager->RegisterAccelerator(
        ui::Accelerator(entry.keycode, entry.modifiers),
        ui::AcceleratorManager::kNormalPriority, accelerator_target_.get());
    accelerator_target_->RegisterAccelerator(
        ui::Accelerator(entry.keycode, entry.modifiers), entry.action);
  }

  auto* window = widget_->GetNativeWindow();
  GrantPermissionToActivateIndefinitely(window);

  window->SetProperty(chromeos::kSupportsFloatedStateKey,
                      supports_floated_state_);
}

float ClientControlledShellSurface::GetScale() const {
  return !use_default_scale_cancellation_
             ? ShellSurfaceBase::GetScaleFactor()
             : ::exo::GetDefaultDeviceScaleFactor();
}

float ClientControlledShellSurface::GetScaleFactor() const {
  // TODO(andreaorru): consolidate Scale and ScaleFactor.
  return GetScale();
}

std::optional<gfx::Rect> ClientControlledShellSurface::GetWidgetBounds() const {
  const ash::NonClientFrameViewAsh* frame_view = GetFrameView();
  if (frame_view->GetFrameEnabled() && !frame_view->GetFrameOverlapped()) {
    gfx::Rect visible_bounds = GetVisibleBounds();
    if (widget_->IsMaximized() && frame_type_ == SurfaceFrameType::NORMAL) {
      // When the widget is maximized in clamshell mode, client sends
      // |geometry_| without taking caption height into account.
      visible_bounds.Offset(0, frame_view->NonClientTopBorderHeight());
    }
    return frame_view->GetWindowBoundsForClientBounds(visible_bounds);
  }

  // When frame is overlapped with the client window, widget bounds is the same
  // as the |geometry_| from client.
  return GetVisibleBounds();
}

gfx::Point ClientControlledShellSurface::GetSurfaceOrigin() const {
  return gfx::Point();
}

bool ClientControlledShellSurface::OnPreWidgetCommit() {
  if (!widget_) {
    // Modify the |origin_| to the |pending_geometry_| to place the window on
    // the intended display. See b/77472684 for details.
    // TODO(domlaskowski): Remove this once clients migrate to geometry API with
    // explicit target display.
    if (!pending_geometry_.IsEmpty())
      origin_ = pending_geometry_.origin();
    CreateShellSurfaceWidget(
        chromeos::ToWindowShowState(pending_window_state_));
  }

  // Finish ignoring obsolete bounds update as the state changes caused by
  // display rotation are synced.
  // TODO(takise): This assumes no other bounds update happens during screen
  // rotation. Implement more robust logic to handle synchronization for
  // screen rotation.
  if (pending_geometry_ != geometry_)
    display_rotating_with_pip_ = false;

  ash::WindowState* window_state = GetWindowState();
  state_changed_ = window_state->GetStateType() != pending_window_state_;
  if (!state_changed_) {
    // Animate PIP window movement unless it is being dragged.
    client_controlled_state_->set_next_bounds_change_animation_type(
        window_state->IsPip() && !window_state->is_dragged()
            ? ash::WindowState::BoundsChangeAnimationType::kAnimate
            : ash::WindowState::BoundsChangeAnimationType::kNone);
    return true;
  }

  if (IsPinned(window_state) &&
      (pending_window_state_ == chromeos::WindowStateType::kPinned ||
       pending_window_state_ == chromeos::WindowStateType::kTrustedPinned)) {
    VLOG(1) << "Pinned was requested while pinned";
    return true;
  }

  auto animation_type = ash::WindowState::BoundsChangeAnimationType::kNone;
  switch (pending_window_state_) {
    case chromeos::WindowStateType::kNormal:
      if (widget_->IsMaximized() || widget_->IsFullscreen()) {
        animation_type =
            ash::WindowState::BoundsChangeAnimationType::kCrossFade;
      }
      break;
    case chromeos::WindowStateType::kFloated:
      animation_type =
          ash::WindowState::BoundsChangeAnimationType::kCrossFadeFloat;
      break;
    case chromeos::WindowStateType::kMaximized:
    case chromeos::WindowStateType::kFullscreen:
      if (!window_state->IsPip())
        animation_type =
            ash::WindowState::BoundsChangeAnimationType::kCrossFade;
      break;
    default:
      break;
  }

  if (window_state->IsFloated()) {
    animation_type =
        ash::WindowState::BoundsChangeAnimationType::kCrossFadeUnfloat;
  }

  bool wasPip = window_state->IsPip();
  if (client_controlled_state_->EnterNextState(window_state,
                                               pending_window_state_)) {
    client_controlled_state_->set_next_bounds_change_animation_type(
        animation_type);
  }

  if (wasPip && !window_state->IsMinimized()) {
    // Expanding PIP should end tablet split view (see crbug.com/941788).
    // Clamshell split view does not require special handling. We activate the
    // PIP window, and so overview ends, which means clamshell split view ends.
    // TODO(edcourtney): Consider not ending tablet split view on PIP expand.
    // See crbug.com/950827.
    ash::SplitViewController* split_view_controller =
        ash::SplitViewController::Get(ash::Shell::GetPrimaryRootWindow());
    if (split_view_controller->InTabletSplitViewMode())
      split_view_controller->EndSplitView();
    // As Android doesn't activate PIP tasks after they are expanded, we need
    // to do it here explicitly.
    // TODO(crbug.com/40616384): Investigate if we can activate PIP windows
    // inside commit.
    window_state->Activate();
  }

  return true;
}

void ClientControlledShellSurface::ShowWidget(bool inactive) {
  ScopedDeferWindowStateUpdate update(this);
  ShellSurfaceBase::ShowWidget(inactive);
}

void ClientControlledShellSurface::OnPostWidgetCommit() {
  DCHECK(widget_);

  UpdateFrame();
  UpdateBackdrop();

  if (delegate_) {
    // Since the visible bounds are in screen coordinates, do not scale these
    // bounds with the display's scale before sending them.
    // TODO(b/167286795): Instead of sending bounds in screen coordinates, send
    // the bounds in the display along with the display information, similar to
    // the bounds_changed_callback_.
    delegate_->OnGeometryChanged(GetVisibleBounds());
  }

  // Apply new top inset height.
  if (pending_top_inset_height_ != top_inset_height_) {
    widget_->GetNativeWindow()->SetProperty(aura::client::kTopViewInset,
                                            pending_top_inset_height_);
    top_inset_height_ = pending_top_inset_height_;
  }

  widget_->GetNativeWindow()->SetProperty(aura::client::kZOrderingKey,
                                          pending_always_on_top_
                                              ? ui::ZOrderLevel::kFloatingWindow
                                              : ui::ZOrderLevel::kNormal);

  UpdateResizability();

  ash::WindowState* window_state = GetWindowState();
  // For PIP, the snap fraction is used to specify the ideal position. Usually
  // this value is set in CompleteDrag, but for the initial position, we need
  // to set it here, when the transition is completed.
  if (window_state->IsPip() &&
      !ash::PipPositioner::HasSnapFraction(window_state)) {
    ash::PipPositioner::SaveSnapFraction(
        window_state, window_state->window()->GetBoundsInScreen());
  }

  ShellSurfaceBase::OnPostWidgetCommit();
}

void ClientControlledShellSurface::OnSurfaceDestroying(Surface* surface) {
  if (client_controlled_state_) {
    client_controlled_state_->ResetDelegate();
    client_controlled_state_ = nullptr;
  }
  ShellSurfaceBase::OnSurfaceDestroying(surface);
}

////////////////////////////////////////////////////////////////////////////////
// ClientControlledShellSurface, private:

void ClientControlledShellSurface::UpdateFrame() {
  if (!widget_)
    return;
  ash::WindowState* window_state = GetWindowState();
  bool enable_wide_frame = false;
  if (GetFrameView()->GetFrameEnabled() &&
      window_state->IsMaximizedOrFullscreenOrPinned()) {
    gfx::Rect ideal_bounds =
        GetIdealBoundsForMaximizedOrFullscreenOrPinnedState(
            widget_->GetNativeWindow());
    enable_wide_frame = ideal_bounds.width() != geometry().width();
  }
  bool update_frame = state_changed_;
  state_changed_ = false;
  if (enable_wide_frame) {
    if (!wide_frame_) {
      update_frame = true;
      wide_frame_ = std::make_unique<ash::WideFrameView>(widget_);
      chromeos::ImmersiveFullscreenController::EnableForWidget(widget_, false);
      wide_frame_->Init(immersive_fullscreen_controller_.get());
      wide_frame_->header_view()->GetFrameHeader()->SetFrameTextOverride(
          GetFrameView()
              ->GetHeaderView()
              ->GetFrameHeader()
              ->frame_text_override());
      wide_frame_->GetWidget()->Show();

      // Restoring window targeter replaced by ImmersiveFullscreenController.
      InstallCustomWindowTargeter();

      UpdateCaptionButtonModel();
    }
    DCHECK_EQ(chromeos::FrameHeader::Get(widget_),
              wide_frame_->header_view()->GetFrameHeader());
  } else {
    if (wide_frame_) {
      update_frame = true;
      chromeos::ImmersiveFullscreenController::EnableForWidget(widget_, false);
      wide_frame_.reset();
      GetFrameView()->InitImmersiveFullscreenControllerForView(
          immersive_fullscreen_controller_.get());
      // Restoring window targeter replaced by ImmersiveFullscreenController.
      InstallCustomWindowTargeter();

      UpdateCaptionButtonModel();
    }
    DCHECK_EQ(chromeos::FrameHeader::Get(widget_),
              GetFrameView()->GetHeaderView()->GetFrameHeader());
    UpdateFrameWidth();
  }
  // The autohide should be applied when the window state is in
  // maximzied, fullscreen or pinned. Update the auto hide state
  // inside commit.
  if (update_frame)
    UpdateAutoHideFrame();
}

void ClientControlledShellSurface::UpdateCaptionButtonModel() {
  auto model = std::make_unique<CaptionButtonModel>(frame_visible_button_mask_,
                                                    frame_enabled_button_mask_);
  if (wide_frame_)
    wide_frame_->SetCaptionButtonModel(std::move(model));
  else
    GetFrameView()->SetCaptionButtonModel(std::move(model));
}

void ClientControlledShellSurface::UpdateBackdrop() {
  aura::Window* window = widget_->GetNativeWindow();

  // Always create a backdrop regardless of the geometry because
  // maximized/fullscreen widget's geometry can be cropped.
  bool enable_backdrop = widget_->IsFullscreen() || widget_->IsMaximized();

  ash::WindowBackdrop::BackdropMode target_backdrop_mode =
      enable_backdrop ? ash::WindowBackdrop::BackdropMode::kEnabled
                      : ash::WindowBackdrop::BackdropMode::kAuto;

  ash::WindowBackdrop* window_backdrop = ash::WindowBackdrop::Get(window);
  if (window_backdrop->mode() != target_backdrop_mode)
    window_backdrop->SetBackdropMode(target_backdrop_mode);
}

void ClientControlledShellSurface::UpdateFrameWidth() {
  int width = -1;
  if (shadow_bounds_) {
    float device_scale_factor =
        GetWidget()->GetNativeWindow()->layer()->device_scale_factor();
    float dsf_to_default_dsf = device_scale_factor / GetScale();
    width = base::ClampRound(shadow_bounds_->width() * dsf_to_default_dsf);
  }
  static_cast<chromeos::HeaderView*>(GetFrameView()->GetHeaderView())
      ->SetWidthInPixels(width);
}

void ClientControlledShellSurface::UpdateFrameType() {
  if (container_ == ash::kShellWindowId_SystemModalContainer &&
      pending_frame_type_ != SurfaceFrameType::NONE) {
    LOG(WARNING)
        << "A surface in system modal container should not have a frame:"
        << static_cast<int>(pending_frame_type_);
    return;
  }

  // TODO(oshima): We shouldn't send the synthesized motion event when just
  // changing the frame type. The better solution would be to keep the window
  // position regardless of the frame state, but that won't be available until
  // next arc version.
  // This is a stopgap solution not to generate the event until it is resolved.
  EventTargetingBlocker blocker;
  bool suppress_mouse_event = frame_type_ != pending_frame_type_ && widget_;
  if (suppress_mouse_event)
    blocker.Block(widget_->GetNativeWindow());
  ShellSurfaceBase::OnSetFrame(pending_frame_type_);
  UpdateAutoHideFrame();

  if (suppress_mouse_event)
    UpdateHostWindowOrigin();
}

bool ClientControlledShellSurface::GetCanResizeFromSizeConstraints() const {
  // Both min and max bounds of unresizable, maximized ARC windows are empty
  // because Ash requires maximizable apps have empty max bounds.
  // This assumes that ARC sets non-empty min sizes to all resizable apps.
  //
  // Example values of size constraints:
  // ----------------------------------------------------------------------
  // |           |          resizable           |      non-resizable      |
  // ----------------------------------------------------------------------
  // | freeform  | min: (400, 400), max: (0, 0) | min = max = window size |
  // ----------------------------------------------------------------------
  // | maximized | min: (400, 400), max: (0, 0) |   min = max = (0, 0)    |
  // ----------------------------------------------------------------------

  return requested_minimum_size_ != requested_maximum_size_;
}

void ClientControlledShellSurface::
    EnsureCompositorIsLockedForOrientationChange() {
  if (!orientation_compositor_lock_) {
    ui::Compositor* compositor =
        widget_->GetNativeWindow()->layer()->GetCompositor();
    orientation_compositor_lock_ = compositor->GetCompositorLock(
        this, base::Milliseconds(kOrientationLockTimeoutMs));
  }
}

ash::WindowState* ClientControlledShellSurface::GetWindowState() {
  return ash::WindowState::Get(widget_->GetNativeWindow());
}

ash::NonClientFrameViewAsh* ClientControlledShellSurface::GetFrameView() {
  return static_cast<ash::NonClientFrameViewAsh*>(
      widget_->non_client_view()->frame_view());
}

const ash::NonClientFrameViewAsh* ClientControlledShellSurface::GetFrameView()
    const {
  return static_cast<const ash::NonClientFrameViewAsh*>(
      widget_->non_client_view()->frame_view());
}

float ClientControlledShellSurface::GetClientToDpPendingScale() const {
  // When the client is scale-aware, we expect that it will resize windows when
  // reacting to scale changes. Since we do not commit the scale until the
  // buffer size changes, any bounds sent after a scale change and before the
  // scale commit will result in mismatched sizes between widget and the buffer.
  // To work around this, we use pending scale factor to calculate bounds in DP
  // instead of GetClientToDpScale().
  return use_default_scale_cancellation_ ? 1.f : 1.f / GetPendingScaleFactor();
}

gfx::Rect
ClientControlledShellSurface::GetClientBoundsForWindowBoundsAndWindowState(
    const gfx::Rect& window_bounds,
    chromeos::WindowStateType window_state) const {
  // The client's geometry uses fullscreen in client controlled,
  // (but the surface is placed under the frame), so just use
  // the window bounds instead for maximixed state.
  // Snapped window states in tablet mode do not include the caption height.
  const bool is_snapped =
      window_state == chromeos::WindowStateType::kPrimarySnapped ||
      window_state == chromeos::WindowStateType::kSecondarySnapped;
  const bool is_maximized =
      window_state == chromeos::WindowStateType::kMaximized;

  if (is_maximized ||
      (is_snapped && display::Screen::GetScreen()->InTabletMode())) {
    return window_bounds;
  }

  gfx::Rect client_bounds =
      GetFrameView()->GetFrameOverlapped()
          ? window_bounds
          : GetFrameView()->GetClientBoundsForWindowBounds(window_bounds);

  if (is_snapped && display::Screen::GetScreen()->GetTabletState() ==
                        display::TabletState::kExitingTabletMode) {
    // Until the next commit, the frame view is in immersive mode, and the above
    // GetClientBoundsForWindowBounds doesn't return bounds taking the caption
    // height into account.
    client_bounds.Inset(gfx::Insets().set_top(
        GetFrameView()->NonClientTopBorderPreferredHeight()));
  }
  return client_bounds;
}

// static
void ClientControlledShellSurface::
    SetClientControlledStateDelegateFactoryForTest(
        const DelegateFactoryCallback& callback) {
  auto& factory = GetFactoryForTesting();
  factory = callback;
}

}  // namespace exo