chromium/chrome/browser/ui/views/apps/chrome_native_app_window_views_aura_ash.cc

// Copyright 2016 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/ui/views/apps/chrome_native_app_window_views_aura_ash.h"

#include <utility>

#include "apps/ui/views/app_window_frame_view.h"
#include "ash/frame/non_client_frame_view_ash.h"
#include "ash/public/cpp/shell_window_ids.h"
#include "ash/public/cpp/tablet_mode.h"
#include "ash/public/cpp/window_backdrop.h"
#include "ash/public/cpp/window_properties.h"
#include "ash/utility/wm_util.h"
#include "base/check.h"
#include "base/functional/bind.h"
#include "base/scoped_observation.h"
#include "base/trace_event/trace_event.h"
#include "chrome/browser/apps/app_service/app_service_proxy.h"
#include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
#include "chrome/browser/apps/icon_standardizer.h"
#include "chrome/browser/ash/note_taking/note_taking_helper.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/ash/multi_user/multi_user_context_menu.h"
#include "chrome/browser/ui/views/exclusive_access_bubble_views.h"
#include "chrome/common/extensions/extension_constants.h"
#include "chromeos/components/mgs/managed_guest_session_utils.h"
#include "chromeos/constants/chromeos_features.h"
#include "chromeos/ui/base/app_types.h"
#include "chromeos/ui/base/chromeos_ui_constants.h"
#include "chromeos/ui/base/window_properties.h"
#include "chromeos/ui/base/window_state_type.h"
#include "chromeos/ui/frame/frame_utils.h"
#include "chromeos/ui/frame/immersive/immersive_fullscreen_controller.h"
#include "components/app_restore/app_restore_utils.h"
#include "components/app_restore/window_properties.h"
#include "components/services/app_service/public/cpp/app_types.h"
#include "extensions/browser/app_window/app_delegate.h"
#include "ui/aura/client/aura_constants.h"
#include "ui/aura/window_observer.h"
#include "ui/base/hit_test.h"
#include "ui/base/models/image_model.h"
#include "ui/base/models/simple_menu_model.h"
#include "ui/base/ui_base_types.h"
#include "ui/display/screen.h"
#include "ui/display/tablet_state.h"
#include "ui/events/event_constants.h"
#include "ui/gfx/geometry/rounded_corners_f.h"
#include "ui/gfx/geometry/skia_conversions.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/views/controls/menu/menu_runner.h"
#include "ui/views/controls/webview/webview.h"
#include "ui/views/widget/widget.h"
#include "ui/views/window/client_view.h"
#include "ui/wm/core/coordinate_conversion.h"

using extensions::AppWindow;
namespace {

// NonClientFrameView implementation for frameless chrome apps (i.e apps that do
// not use ash style NonClientFrameView).
class NativeAppWindowFrameView : public apps::AppWindowFrameView,
                                 public aura::WindowObserver {
 public:
  NativeAppWindowFrameView(views::Widget* widget,
                           ChromeNativeAppWindowViewsAuraAsh* app_window,
                           bool draw_frame,
                           const SkColor& active_frame_color,
                           const SkColor& inactive_frame_color)
      : apps::AppWindowFrameView(widget,
                                 app_window,
                                 draw_frame,
                                 active_frame_color,
                                 inactive_frame_color) {
    frame_window_observation_.Observe(widget->GetNativeWindow());
  }

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

  ~NativeAppWindowFrameView() override = default;

  // views::NonClientFrameView
  void UpdateWindowRoundedCorners() override {
    DCHECK(GetWidget());

    if (!chromeos::features::IsRoundedWindowsEnabled()) {
      return;
    }

    aura::Window* frame_window = GetWidget()->GetNativeWindow();

    const int corner_radius = chromeos::GetFrameCornerRadius(frame_window);
    frame_window->SetProperty(aura::client::kWindowCornerRadiusKey,
                              corner_radius);

    if (draw_frame()) {
      SetFrameCornerRadius(corner_radius);
    }

    GetWidget()->client_view()->UpdateWindowRoundedCorners(corner_radius);
  }

  // aura::WindowObserver:
  void OnWindowPropertyChanged(aura::Window* window,
                               const void* key,
                               intptr_t old) override {
    // Windows in ChromeOS are rounded for certain window states. If these
    // states change, we need to update the rounded corners accordingly. See
    // `chromeos::ShouldWindowHaveRoundedCorners()` for more details.
    if (chromeos::CanPropertyEffectFrameRadius(key)) {
      UpdateWindowRoundedCorners();
    }
  }

  void OnWindowDestroyed(aura::Window* window) override {
    frame_window_observation_.Reset();
  }

 private:
  base::ScopedObservation<aura::Window, aura::WindowObserver>
      frame_window_observation_{this};
};

class ChromeNativeAppNonClientView : public views::ClientView {
 public:
  ChromeNativeAppNonClientView(views::Widget* frame,
                               ChromeNativeAppWindowViewsAuraAsh* app_window,
                               bool has_non_standard_frame,
                               bool draw_non_standard_frame)
      : views::ClientView(frame, app_window),
        has_non_standard_frame_(has_non_standard_frame),
        draw_non_standard_frame_(draw_non_standard_frame) {}

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

  ~ChromeNativeAppNonClientView() override = default;

  // views::ClientView:
  void UpdateWindowRoundedCorners(int corner_radius) override {
    DCHECK(GetWidget());

    gfx::RoundedCornersF radii(0, 0, corner_radius, corner_radius);

    // If the chrome app's non-standard frame is not drawn, then round all four
    // corners of the web contents to achieve a rounded window.
    // For an app with a standard frame, we always draw the frame.
    if (has_non_standard_frame_ && !draw_non_standard_frame_) {
      radii.set_upper_right(corner_radius);
      radii.set_upper_left(corner_radius);
    }

    static_cast<ChromeNativeAppWindowViewsAuraAsh*>(contents_view())
        ->web_view()
        ->holder()
        ->SetCornerRadii(radii);
  }

 private:
  const bool has_non_standard_frame_ = true;
  const bool draw_non_standard_frame_ = true;
};

}  // namespace

ChromeNativeAppWindowViewsAuraAsh::ChromeNativeAppWindowViewsAuraAsh() =
    default;

ChromeNativeAppWindowViewsAuraAsh::~ChromeNativeAppWindowViewsAuraAsh() =
    default;

///////////////////////////////////////////////////////////////////////////////
// NativeAppWindowViews implementation:
void ChromeNativeAppWindowViewsAuraAsh::InitializeWindow(
    AppWindow* app_window,
    const AppWindow::CreateParams& create_params) {
  ChromeNativeAppWindowViewsAura::InitializeWindow(app_window, create_params);
  aura::Window* window = GetNativeWindow();

  // Fullscreen doesn't always imply immersive mode (see
  // ShouldEnableImmersive()).
  window->SetProperty(chromeos::kImmersiveImpliedByFullscreen, false);
  // TODO(crbug.com/41478054): Determine if all non-resizable windows
  // should have this behavior, or just the feedback app.
  window_observation_.Observe(window);
}

///////////////////////////////////////////////////////////////////////////////
// ChromeNativeAppWindowViewsAura implementation:
void ChromeNativeAppWindowViewsAuraAsh::OnBeforeWidgetInit(
    const AppWindow::CreateParams& create_params,
    views::Widget::InitParams* init_params,
    views::Widget* widget) {
  ChromeNativeAppWindowViewsAura::OnBeforeWidgetInit(create_params, init_params,
                                                     widget);
  // Some windows need to be placed in special containers, for example to make
  // them visible at the login or lock screen.
  std::optional<int> container_id;
  if (create_params.is_ime_window)
    container_id = ash::kShellWindowId_ImeWindowParentContainer;
  else if (create_params.show_on_lock_screen)
    container_id = ash::kShellWindowId_LockActionHandlerContainer;

  if (container_id.has_value()) {
    ash_util::SetupWidgetInitParamsForContainer(init_params, *container_id);
    if (!ash::IsActivatableShellWindowId(*container_id)) {
      // This ensures calls to Activate() don't attempt to activate the window
      // locally, which can have side effects that should be avoided (such as
      // changing focus). See https://crbug.com/935274 for more details.
      init_params->activatable = views::Widget::InitParams::Activatable::kNo;
    }
  }

  // Resizable lock screen apps will end up maximized by ash. Do it now to
  // save back-and-forth communication with the window manager. Right now all
  // lock screen apps either end up maximized (e.g. Keep) or are not resizable.
  if (create_params.show_on_lock_screen && create_params.resizable) {
    DCHECK_EQ(ui::SHOW_STATE_DEFAULT, init_params->show_state);
    init_params->show_state = ui::SHOW_STATE_MAXIMIZED;
  }

  const int32_t restore_window_id =
      app_restore::FetchRestoreWindowId(app_window()->extension_id());
  init_params->init_properties_container.SetProperty(
      app_restore::kWindowIdKey, app_window()->session_id().id());
  init_params->init_properties_container.SetProperty(
      app_restore::kRestoreWindowIdKey, restore_window_id);
  init_params->init_properties_container.SetProperty(
      app_restore::kAppIdKey, app_window()->extension_id());
  init_params->init_properties_container.SetProperty(
      chromeos::kAppTypeKey, chromeos::AppType::CHROME_APP);

  app_restore::ModifyWidgetParams(restore_window_id, init_params);
}

std::unique_ptr<views::NonClientFrameView>
ChromeNativeAppWindowViewsAuraAsh::CreateNonStandardAppFrame() {
  auto frame = std::make_unique<NativeAppWindowFrameView>(
      widget(), this, HasFrameColor(), ActiveFrameColor(),
      InactiveFrameColor());
  frame->Init();

  // For Aura windows on the Ash desktop the sizes are different and the user
  // can resize the window from slightly outside the bounds as well.
  frame->SetResizeSizes(chromeos::kResizeInsideBoundsSize,
                        chromeos::kResizeOutsideBoundsSize,
                        chromeos::kResizeAreaCornerSize);
  return frame;
}

ui::ImageModel ChromeNativeAppWindowViewsAuraAsh::GetWindowIcon() {
  TRACE_EVENT0("ui", "ChromeNativeAppWindowViewsAuraAsh::GetWindowIcon");
  const ui::ImageModel& image = ChromeNativeAppWindowViews::GetWindowIcon();
  if (image.IsEmpty())
    return ui::ImageModel();

  DCHECK(image.IsImage());
  const gfx::ImageSkia image_skia = image.Rasterize(nullptr);
  return ui::ImageModel::FromImageSkia(
      apps::CreateStandardIconImage(image_skia));
}

bool ChromeNativeAppWindowViewsAuraAsh::ShouldRemoveStandardFrame() {
  if (IsFrameless())
    return true;

  return HasFrameColor();
}

void ChromeNativeAppWindowViewsAuraAsh::EnsureAppIconCreated() {
  TRACE_EVENT0("ui", "ChromeNativeAppWindowViewsAuraAsh::EnsureAppIconCreated");
  LoadAppIcon(true /* allow_placeholder_icon */);
}

gfx::RoundedCornersF ChromeNativeAppWindowViewsAuraAsh::GetWindowRadii() const {
  if (!GetNativeWindow() || !chromeos::features::IsRoundedWindowsEnabled()) {
    return gfx::RoundedCornersF();
  }

  const int corner_radius =
      GetNativeWindow()->GetProperty(aura::client::kWindowCornerRadiusKey);
  return gfx::RoundedCornersF(corner_radius);
}

gfx::Rect ChromeNativeAppWindowViewsAuraAsh::GetRestoredBounds() const {
  gfx::Rect* bounds =
      GetNativeWindow()->GetProperty(ash::kRestoreBoundsOverrideKey);
  if (bounds && !bounds->IsEmpty())
    return *bounds;

  return ChromeNativeAppWindowViewsAura::GetRestoredBounds();
}

ui::WindowShowState
ChromeNativeAppWindowViewsAuraAsh::GetRestoredState() const {
  // Use kRestoreShowStateKey to get the window restore show state in case a
  // window is minimized/hidden.
  ui::WindowShowState restore_state =
      GetNativeWindow()->GetProperty(aura::client::kRestoreShowStateKey);

  bool is_fullscreen = false;
  if (GetNativeWindow()->GetProperty(ash::kRestoreBoundsOverrideKey)) {
    // If an override is given, use that restore state, unless the window is in
    // immersive fullscreen.
    restore_state = chromeos::ToWindowShowState(GetNativeWindow()->GetProperty(
        ash::kRestoreWindowStateTypeOverrideKey));
    is_fullscreen = restore_state == ui::SHOW_STATE_FULLSCREEN;
  } else {
    if (IsMaximized())
      return ui::SHOW_STATE_MAXIMIZED;
    is_fullscreen = IsFullscreen();
  }

  if (is_fullscreen) {
    if (IsImmersiveModeEnabled()) {
      // Restore windows which were previously in immersive fullscreen to their
      // pre-fullscreen state. Restoring the window to a different fullscreen
      // type makes for a bad experience.
      return GetNativeWindow()->GetProperty(aura::client::kRestoreShowStateKey);
    }
    return ui::SHOW_STATE_FULLSCREEN;
  }

  return GetRestorableState(restore_state);
}

ui::ZOrderLevel ChromeNativeAppWindowViewsAuraAsh::GetZOrderLevel() const {
  return widget()->GetZOrderLevel();
}

///////////////////////////////////////////////////////////////////////////////
// views::ContextMenuController implementation:
void ChromeNativeAppWindowViewsAuraAsh::ShowContextMenuForViewImpl(
    views::View* source,
    const gfx::Point& p,
    ui::MenuSourceType source_type) {
  menu_model_ = CreateMultiUserContextMenu(GetNativeWindow());
  if (!menu_model_)
    return;

  // Only show context menu if point is in caption.
  gfx::Point point_in_view_coords(p);
  views::View::ConvertPointFromScreen(widget()->non_client_view(),
                                      &point_in_view_coords);
  int hit_test =
      widget()->non_client_view()->NonClientHitTest(point_in_view_coords);
  if (hit_test == HTCAPTION) {
    menu_runner_ = std::make_unique<views::MenuRunner>(
        menu_model_.get(),
        views::MenuRunner::HAS_MNEMONICS | views::MenuRunner::CONTEXT_MENU,
        base::BindRepeating(&ChromeNativeAppWindowViewsAuraAsh::OnMenuClosed,
                            base::Unretained(this)));
    menu_runner_->RunMenuAt(source->GetWidget(), nullptr,
                            gfx::Rect(p, gfx::Size(0, 0)),
                            views::MenuAnchorPosition::kTopLeft, source_type);
  } else {
    menu_model_.reset();
  }
}

///////////////////////////////////////////////////////////////////////////////
// WidgetDelegate implementation:
std::unique_ptr<views::NonClientFrameView>
ChromeNativeAppWindowViewsAuraAsh::CreateNonClientFrameView(
    views::Widget* widget) {
  if (IsFrameless())
    return CreateNonStandardAppFrame();

  window_state_observation_.Observe(ash::WindowState::Get(GetNativeWindow()));

  auto custom_frame_view = std::make_unique<ash::NonClientFrameViewAsh>(widget);

  custom_frame_view->GetHeaderView()->set_context_menu_controller(this);

  // Enter immersive mode if the app is opened in tablet mode with the hide
  // titlebars feature enabled.
  UpdateImmersiveMode();

  if (HasFrameColor()) {
    custom_frame_view->SetFrameColors(ActiveFrameColor(),
                                      InactiveFrameColor());
  }

  return custom_frame_view;
}

views::ClientView* ChromeNativeAppWindowViewsAuraAsh::CreateClientView(
    views::Widget* widget) {
  return new ChromeNativeAppNonClientView(
      widget, this,
      /*has_non_standard_frame=*/IsFrameless(),
      /*draw_non_standard_frame=*/HasFrameColor());
}

///////////////////////////////////////////////////////////////////////////////
// NativeAppWindow implementation:
void ChromeNativeAppWindowViewsAuraAsh::SetFullscreen(int fullscreen_types) {
  ChromeNativeAppWindowViewsAura::SetFullscreen(fullscreen_types);
  UpdateImmersiveMode();

  // In a managed guest session, display a toast with instructions on exiting
  // fullscreen.
  if (chromeos::IsManagedGuestSession()) {
    UpdateExclusiveAccessBubble(
        {.type = fullscreen_types & (AppWindow::FULLSCREEN_TYPE_HTML_API |
                                     AppWindow::FULLSCREEN_TYPE_WINDOW_API)
                     ? EXCLUSIVE_ACCESS_BUBBLE_TYPE_FULLSCREEN_EXIT_INSTRUCTION
                     : EXCLUSIVE_ACCESS_BUBBLE_TYPE_NONE},
        base::NullCallback());
  }

  // Autohide the shelf instead of hiding it completely for OS fullscreen.
  const bool should_hide_shelf =
      fullscreen_types != AppWindow::FULLSCREEN_TYPE_OS;
  widget()->GetNativeWindow()->SetProperty(
      chromeos::kHideShelfWhenFullscreenKey, should_hide_shelf);

  // Invalidate the frame to ensure that it is re-laid out (even if the bounds
  // don't change) so that the frame sets the bounds of the client view.
  widget()->non_client_view()->frame_view()->InvalidateLayout();
}

void ChromeNativeAppWindowViewsAuraAsh::SetActivateOnPointer(
    bool activate_on_pointer) {
  widget()->GetNativeWindow()->SetProperty(aura::client::kActivateOnPointerKey,
                                           activate_on_pointer);
}

///////////////////////////////////////////////////////////////////////////////
// display::DisplayObserver implementation:
void ChromeNativeAppWindowViewsAuraAsh::OnDisplayTabletStateChanged(
    display::TabletState state) {
  switch (state) {
    case display::TabletState::kEnteringTabletMode:
    case display::TabletState::kExitingTabletMode:
      break;
    case display::TabletState::kInTabletMode:
      OnTabletModeToggled(true);
      break;
    case display::TabletState::kInClamshellMode:
      OnTabletModeToggled(false);
      break;
  }
}

///////////////////////////////////////////////////////////////////////////////
// ui::AcceleratorProvider implementation:
bool ChromeNativeAppWindowViewsAuraAsh::GetAcceleratorForCommandId(
    int command_id,
    ui::Accelerator* accelerator) const {
  // Normally |accelerator| is used to determine the text in the bubble;
  // however, for the fullscreen type set in SetFullscreen(), the bubble
  // currently ignores it, and will always use IDS_APP_ESC_KEY. Be explicit here
  // anyway.
  *accelerator = ui::Accelerator(ui::KeyboardCode::VKEY_ESCAPE, ui::EF_NONE);
  return true;
}

///////////////////////////////////////////////////////////////////////////////
// ExclusiveAccessContext implementation:
Profile* ChromeNativeAppWindowViewsAuraAsh::GetProfile() {
  return Profile::FromBrowserContext(web_view()->GetBrowserContext());
}

bool ChromeNativeAppWindowViewsAuraAsh::IsFullscreen() const {
  return NativeAppWindowViews::IsFullscreen();
}

void ChromeNativeAppWindowViewsAuraAsh::EnterFullscreen(
    const GURL& url,
    ExclusiveAccessBubbleType bubble_type,
    const int64_t display_id) {
  // This codepath is never hit for Chrome Apps.
  NOTREACHED();
}

void ChromeNativeAppWindowViewsAuraAsh::ExitFullscreen() {
  // This codepath is never hit for Chrome Apps.
  NOTREACHED();
}

void ChromeNativeAppWindowViewsAuraAsh::UpdateExclusiveAccessBubble(
    const ExclusiveAccessBubbleParams& params,
    ExclusiveAccessBubbleHideCallback first_hide_callback) {
  if (params.type == EXCLUSIVE_ACCESS_BUBBLE_TYPE_NONE &&
      !params.has_download) {
    exclusive_access_bubble_.reset();
    if (first_hide_callback) {
      std::move(first_hide_callback)
          .Run(ExclusiveAccessBubbleHideReason::kNotShown);
    }
    return;
  }

  if (exclusive_access_bubble_) {
    exclusive_access_bubble_->Update(params, std::move(first_hide_callback));
    return;
  }

  exclusive_access_bubble_ = std::make_unique<ExclusiveAccessBubbleViews>(
      this, params, std::move(first_hide_callback));
}

bool ChromeNativeAppWindowViewsAuraAsh::IsExclusiveAccessBubbleDisplayed()
    const {
  return exclusive_access_bubble_ && exclusive_access_bubble_->IsShowing();
}

void ChromeNativeAppWindowViewsAuraAsh::OnExclusiveAccessUserInput() {
  if (exclusive_access_bubble_)
    exclusive_access_bubble_->OnUserInput();
}

content::WebContents*
ChromeNativeAppWindowViewsAuraAsh::GetWebContentsForExclusiveAccess() {
  return web_view()->web_contents();
}

bool ChromeNativeAppWindowViewsAuraAsh::CanUserExitFullscreen() const {
  return true;
}

///////////////////////////////////////////////////////////////////////////////
// ExclusiveAccessBubbleViewsContext implementation:
ExclusiveAccessManager*
ChromeNativeAppWindowViewsAuraAsh::GetExclusiveAccessManager() {
  return exclusive_access_manager_.get();
}

ui::AcceleratorProvider*
ChromeNativeAppWindowViewsAuraAsh::GetAcceleratorProvider() {
  return this;
}

gfx::NativeView ChromeNativeAppWindowViewsAuraAsh::GetBubbleParentView() const {
  return widget()->GetNativeView();
}

gfx::Rect ChromeNativeAppWindowViewsAuraAsh::GetClientAreaBoundsInScreen()
    const {
  return widget()->GetClientAreaBoundsInScreen();
}

bool ChromeNativeAppWindowViewsAuraAsh::IsImmersiveModeEnabled() const {
  return GetWidget()->GetNativeWindow()->GetProperty(
      chromeos::kImmersiveIsActive);
}

gfx::Rect ChromeNativeAppWindowViewsAuraAsh::GetTopContainerBoundsInScreen() {
  gfx::Rect* bounds = GetWidget()->GetNativeWindow()->GetProperty(
      chromeos::kImmersiveTopContainerBoundsInScreen);
  return bounds ? *bounds : gfx::Rect();
}

void ChromeNativeAppWindowViewsAuraAsh::DestroyAnyExclusiveAccessBubble() {
  exclusive_access_bubble_.reset();
}

void ChromeNativeAppWindowViewsAuraAsh::OnWidgetActivationChanged(
    views::Widget* widget,
    bool active) {
  ChromeNativeAppWindowViewsAura::OnWidgetActivationChanged(widget, active);
  // In splitview, minimized windows go back into the overview grid. If we
  // minimize by using the minimize button on the immersive header, the
  // overview window will calculate the title bar offset and the window will be
  // missing its top portion. Prevent this by disabling immersive mode upon
  // minimize.
  UpdateImmersiveMode();
}

void ChromeNativeAppWindowViewsAuraAsh::OnPostWindowStateTypeChange(
    ash::WindowState* window_state,
    chromeos::WindowStateType old_type) {
  DCHECK(!IsFrameless());
  DCHECK_EQ(GetNativeWindow(), window_state->window());
  if (window_state->IsFullscreen() != app_window()->IsFullscreen()) {
    // Report OS-initiated state changes to |app_window()|. This is done in
    // OnPostWindowStateTypeChange rather than OnWindowPropertyChanged because
    // WindowState saves restore bounds *after* changing the property, and
    // enabling immersive mode will change the current bounds before the old
    // bounds can be saved.
    if (window_state->IsFullscreen())
      app_window()->OSFullscreen();
    else
      app_window()->OnNativeWindowChanged();
  }
}

void ChromeNativeAppWindowViewsAuraAsh::OnWindowPropertyChanged(
    aura::Window* window,
    const void* key,
    intptr_t old) {
  if (key != aura::client::kShowStateKey)
    return;

  auto new_state = window->GetProperty(aura::client::kShowStateKey);

  if (new_state != ui::SHOW_STATE_FULLSCREEN &&
      new_state != ui::SHOW_STATE_MINIMIZED && app_window()->IsFullscreen()) {
    app_window()->Restore();
  }

  // Usually OnNativeWindowChanged() is called when the window bounds are
  // changed as a result of a state type change. Because the change in
  // state type has already occurred, we need to call
  // OnNativeWindowChanged() explicitly.
  app_window()->OnNativeWindowChanged();
  UpdateImmersiveMode();
}

void ChromeNativeAppWindowViewsAuraAsh::OnWindowDestroying(
    aura::Window* window) {
  window_state_observation_.Reset();
  DCHECK(window_observation_.IsObservingSource(window));
  window_observation_.Reset();
}

void ChromeNativeAppWindowViewsAuraAsh::OnTabletModeToggled(bool enabled) {
  tablet_mode_enabled_ = enabled;
  UpdateImmersiveMode();
  widget()->non_client_view()->DeprecatedLayoutImmediately();
}

void ChromeNativeAppWindowViewsAuraAsh::OnMenuClosed() {
  menu_runner_.reset();
  menu_model_.reset();
}

bool ChromeNativeAppWindowViewsAuraAsh::ShouldEnableImmersiveMode() const {
  // No immersive mode for forced fullscreen or frameless windows.
  if (app_window()->IsForcedFullscreen() || IsFrameless())
    return false;

  // Always use immersive mode when fullscreen is set by the OS.
  if (app_window()->IsOsFullscreen())
    return true;

  // Windows in tablet mode which are resizable have their title bars
  // hidden in ash for more size, so enable immersive mode so users
  // have access to window controls. Non resizable windows do not gain
  // size by hidding the title bar, so it is not hidden and thus there
  // is no need for immersive mode.
  // TODO(crbug.com/41364538): This adds a little extra animation
  // when minimizing or unminimizing window.
  return display::Screen::GetScreen()->InTabletMode() && CanResize() &&
         !IsMinimized() &&
         GetNativeWindow()->GetProperty(chromeos::kWindowStateTypeKey) !=
             chromeos::WindowStateType::kFloated;
}

void ChromeNativeAppWindowViewsAuraAsh::UpdateImmersiveMode() {
  chromeos::ImmersiveFullscreenController::EnableForWidget(
      widget(), ShouldEnableImmersiveMode());
}

gfx::Image ChromeNativeAppWindowViewsAuraAsh::GetCustomImage() {
  TRACE_EVENT0("ui", "ChromeNativeAppWindowViewsAuraAsh::GetCustomImage");
  gfx::Image image = ChromeNativeAppWindowViews::GetCustomImage();
  return !image.IsEmpty()
             ? gfx::Image(apps::CreateStandardIconImage(image.AsImageSkia()))
             : gfx::Image();
}

gfx::Image ChromeNativeAppWindowViewsAuraAsh::GetAppIconImage() {
  TRACE_EVENT0("ui", "ChromeNativeAppWindowViewsAuraAsh::GetAppIconImage");
  if (!app_icon_image_skia_.isNull())
    return gfx::Image(app_icon_image_skia_);

  return ChromeNativeAppWindowViews::GetAppIconImage();
}

void ChromeNativeAppWindowViewsAuraAsh::LoadAppIcon(
    bool allow_placeholder_icon) {
  TRACE_EVENT0("ui", "ChromeNativeAppWindowViewsAuraAsh::LoadAppIcon");
  if (apps::AppServiceProxyFactory::IsAppServiceAvailableForProfile(
          Profile::FromBrowserContext(app_window()->browser_context()))) {
    apps::AppServiceProxy* proxy = apps::AppServiceProxyFactory::GetForProfile(
        Profile::FromBrowserContext(app_window()->browser_context()));

    auto app_type =
        proxy->AppRegistryCache().GetAppType(app_window()->extension_id());

    if (app_type != apps::AppType::kUnknown) {
      proxy->LoadIcon(
          app_window()->extension_id(), apps::IconType::kStandard,
          app_window()->app_delegate()->PreferredIconSize(),
          allow_placeholder_icon,
          base::BindOnce(&ChromeNativeAppWindowViewsAuraAsh::OnLoadIcon,
                         weak_ptr_factory_.GetWeakPtr()));
    }
  }

  // Ensures the Chrome app icon is created to generate the default app icon.
  // Otherwise, the test cases are broken.
  ChromeNativeAppWindowViews::EnsureAppIconCreated();
}

void ChromeNativeAppWindowViewsAuraAsh::OnLoadIcon(
    apps::IconValuePtr icon_value) {
  TRACE_EVENT0("ui", "ChromeNativeAppWindowViewsAuraAsh::OnLoadIcon");
  if (!icon_value || icon_value->icon_type != apps::IconType::kStandard)
    return;

  app_icon_image_skia_ = icon_value->uncompressed;

  if (icon_value->is_placeholder_icon)
    LoadAppIcon(false /* allow_placeholder_icon */);
}