chromium/ash/components/arc/compat_mode/arc_resize_lock_manager.cc

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

#include "ash/components/arc/compat_mode/arc_resize_lock_manager.h"

#include "ash/components/arc/arc_browser_context_keyed_service_factory_base.h"
#include "ash/components/arc/arc_features.h"
#include "ash/components/arc/compat_mode/arc_splash_screen_dialog_view.h"
#include "ash/components/arc/compat_mode/arc_window_property_util.h"
#include "ash/components/arc/compat_mode/compat_mode_button_controller.h"
#include "ash/components/arc/compat_mode/metrics.h"
#include "ash/components/arc/compat_mode/overlay_dialog.h"
#include "ash/components/arc/compat_mode/touch_mode_mouse_rewriter.h"
#include "ash/game_dashboard/game_dashboard_controller.h"
#include "ash/public/cpp/app_types_util.h"
#include "ash/public/cpp/arc_resize_lock_type.h"
#include "ash/public/cpp/resize_shadow_type.h"
#include "ash/public/cpp/window_properties.h"
#include "ash/shell.h"
#include "ash/wm/resize_shadow_controller.h"
#include "base/functional/bind.h"
#include "base/functional/callback_forward.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/singleton.h"
#include "base/memory/weak_ptr.h"
#include "base/notreached.h"
#include "base/task/sequenced_task_runner.h"
#include "chromeos/ui/base/window_properties.h"
#include "ui/aura/window_observer.h"
#include "ui/aura/window_tree_host.h"
#include "ui/wm/public/activation_client.h"

namespace arc {

namespace {

// Singleton factory for ArcResizeLockManager.
class ArcResizeLockManagerFactory
    : public internal::ArcBrowserContextKeyedServiceFactoryBase<
          ArcResizeLockManager,
          ArcResizeLockManagerFactory> {
 public:
  static constexpr const char* kName = "ArcResizeLockManagerFactory";

  static ArcResizeLockManagerFactory* GetInstance() {
    return base::Singleton<ArcResizeLockManagerFactory>::get();
  }

 private:
  friend struct base::DefaultSingletonTraits<ArcResizeLockManagerFactory>;
  ArcResizeLockManagerFactory() = default;
  ~ArcResizeLockManagerFactory() override = default;
};

// A self-deleting window activation observer that runs the given callback when
// its associated window gets activated.
class WindowActivationObserver : public wm::ActivationChangeObserver,
                                 public aura::WindowObserver {
 public:
  WindowActivationObserver(const WindowActivationObserver&) = delete;
  WindowActivationObserver& operator=(const WindowActivationObserver&) = delete;

  static void RunOnActivated(aura::Window* window,
                             base::OnceClosure on_activated) {
    // ash::Shell can be null in unittests.
    if (!ash::Shell::HasInstance())
      return;

    if (ash::Shell::Get()->activation_client()->GetActiveWindow() == window) {
      std::move(on_activated).Run();
      return;
    }

    // The following instance self-destructs when the window gets activated or
    // destroyed before getting activated.
    new WindowActivationObserver(window, std::move(on_activated));
  }

  // aura::WindowObserver:
  void OnWindowDestroying(aura::Window* window) override {
    DCHECK(observer_.IsObservingSource(window));
    delete this;
  }

  // wm::ActivationChangeObserver:
  void OnWindowActivated(ActivationReason reason,
                         aura::Window* gained_active,
                         aura::Window* lost_active) override {
    if (gained_active != window_)
      return;
    RemoveAllObservers();
    // To avoid nested-activation, here we post the task to the queue.
    base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE, std::move(on_activated_));
    delete this;
  }

 private:
  WindowActivationObserver(aura::Window* window, base::OnceClosure on_activated)
      : window_(window), on_activated_(std::move(on_activated)) {
    DCHECK(!on_activated_.is_null());
    ash::Shell::Get()->activation_client()->AddObserver(this);
    observer_.Observe(window_.get());
  }

  ~WindowActivationObserver() override { RemoveAllObservers(); }

  void RemoveAllObservers() {
    observer_.Reset();
    ash::Shell::Get()->activation_client()->RemoveObserver(this);
  }

  const raw_ptr<aura::Window> window_;
  base::OnceClosure on_activated_;
  base::ScopedObservation<aura::Window, aura::WindowObserver> observer_{this};
};

// A self-deleting window property observer that runs the given callback when
// its ash::kAppIDKey is set to non-null value.
class AppIdObserver : public aura::WindowObserver {
 public:
  AppIdObserver(const AppIdObserver&) = delete;
  AppIdObserver& operator=(const AppIdObserver&) = delete;

  static void RunOnReady(aura::Window* window,
                         base::OnceCallback<void(aura::Window*)> on_ready) {
    if (GetAppId(window)) {
      std::move(on_ready).Run(window);
      return;
    }

    // The following instance self-destructs when the window gets activated or
    // destroyed before getting activated.
    new AppIdObserver(window, std::move(on_ready));
  }

  // aura::WindowObserver:
  void OnWindowDestroying(aura::Window* window) override {
    DCHECK(observer_.IsObservingSource(window));
    delete this;
  }
  void OnWindowPropertyChanged(aura::Window* window,
                               const void* key,
                               intptr_t old) override {
    DCHECK(observer_.IsObservingSource(window));
    if (key != ash::kAppIDKey)
      return;
    if (!GetAppId(window))
      return;
    observer_.Reset();
    std::move(on_ready_).Run(window);
    delete this;
  }

 private:
  AppIdObserver(aura::Window* window,
                base::OnceCallback<void(aura::Window*)> on_ready)
      : window_(window), on_ready_(std::move(on_ready)) {
    DCHECK(!on_ready_.is_null());
    observer_.Observe(window_.get());
  }

  ~AppIdObserver() override { observer_.Reset(); }

  const raw_ptr<aura::Window> window_;
  base::OnceCallback<void(aura::Window*)> on_ready_;
  base::ScopedObservation<aura::Window, aura::WindowObserver> observer_{this};
};

bool ShouldEnableResizeLock(ash::ArcResizeLockType type) {
  return type != ash::ArcResizeLockType::NONE &&
         type != ash::ArcResizeLockType::RESIZE_ENABLED_TOGGLABLE;
}

}  // namespace

// static
ArcResizeLockManager* ArcResizeLockManager::GetForBrowserContext(
    content::BrowserContext* context) {
  return ArcResizeLockManagerFactory::GetForBrowserContext(context);
}

ArcResizeLockManager::ArcResizeLockManager(
    content::BrowserContext* browser_context,
    ArcBridgeService* arc_bridge_service)
    : compat_mode_button_controller_(
          std::make_unique<CompatModeButtonController>()),
      touch_mode_mouse_rewriter_(std::make_unique<TouchModeMouseRewriter>()) {
  if (aura::Env::HasInstance())
    env_observation.Observe(aura::Env::GetInstance());
}

ArcResizeLockManager::~ArcResizeLockManager() = default;

void ArcResizeLockManager::OnWindowInitialized(aura::Window* new_window) {
  if (!ash::IsArcWindow(new_window))
    return;

  if (window_observations_.IsObservingSource(new_window))
    return;

  window_observations_.AddObservation(new_window);

  AppIdObserver::RunOnReady(
      new_window,
      base::BindOnce(
          [](base::WeakPtr<ArcResizeLockManager> manager,
             aura::Window* window) {
            if (!manager)
              return;
            if (!manager->pref_delegate_)
              return;
            const auto state =
                manager->pref_delegate_->GetResizeLockState(*GetAppId(window));
            RecordResizeLockStateHistogram(
                ResizeLockStateHistogramType::InitialState, state);
          },
          weak_ptr_factory_.GetWeakPtr()));
}

void ArcResizeLockManager::OnWindowPropertyChanged(aura::Window* window,
                                                   const void* key,
                                                   intptr_t old) {
  // TODO(b/344640055): Replace below simple fix.
  // The overlay layer might be added before `chromeos::kIsGameKey` is set for
  // splash screen dialog. Once `chromeos::kIsGameKey` is set to true, remove
  // the overlay layer.
  if (key == chromeos::kIsGameKey &&
      window->GetProperty(chromeos::kIsGameKey)) {
    OverlayDialog::CloseIfAny(window);
    return;
  }

  if (key != ash::kArcResizeLockTypeKey)
    return;

  const auto new_value = window->GetProperty(ash::kArcResizeLockTypeKey);
  const auto old_value = static_cast<ash::ArcResizeLockType>(old);

  if (new_value != old_value) {
    AppIdObserver::RunOnReady(
        window, base::BindOnce(
                    [](base::WeakPtr<ArcResizeLockManager> manager,
                       aura::Window* window) {
                      if (!manager)
                        return;
                      if (ShouldEnableResizeLock(window->GetProperty(
                              ash::kArcResizeLockTypeKey))) {
                        manager->EnableResizeLock(window);
                      } else {
                        manager->DisableResizeLock(window);
                      }
                      // EnableResizeLock() and DisableResizeLock() are supposed
                      // to be called only when resizability is toggled while
                      // resize lock state may need to be updated even when
                      // resizability doesn't change (e.g.NONE ->
                      // RESIZE_ENABLED_TOGGLABLE)
                      manager->UpdateResizeLockState(window);
                    },
                    weak_ptr_factory_.GetWeakPtr()));
  }

  // We need to always trigger UpdateCompatModeButton regardless of value
  // change because it need to be called even when the property is set to
  // ArcResizeLockType::NONE, which is the the default value of
  // kArcResizeLockTypeKey, and the new value is the same as |old| in that case.
  AppIdObserver::RunOnReady(
      window, base::BindOnce(&CompatModeButtonController::Update,
                             compat_mode_button_controller_->GetWeakPtr()));
}

void ArcResizeLockManager::Shutdown() {
  compat_mode_button_controller_->ClearPrefDelegate();
  pref_delegate_ = nullptr;
}

void ArcResizeLockManager::OnWindowBoundsChanged(
    aura::Window* window,
    const gfx::Rect& old_bounds,
    const gfx::Rect& new_bounds,
    ui::PropertyChangeReason reason) {
  compat_mode_button_controller_->Update(window);
}

void ArcResizeLockManager::OnWindowDestroying(aura::Window* window) {
  resize_lock_enabled_windows_.erase(window);
  if (window_observations_.IsObservingSource(window))
    window_observations_.RemoveObservation(window);
}

void ArcResizeLockManager::SetPrefDelegate(
    ArcResizeLockPrefDelegate* delegate) {
  CHECK(!pref_delegate_);
  pref_delegate_ = delegate;
  compat_mode_button_controller_->SetPrefDelegate(delegate);
}

void ArcResizeLockManager::EnableResizeLock(aura::Window* window) {
  const bool inserted = resize_lock_enabled_windows_.insert(window).second;
  if (!inserted)
    return;

  // TODO(tetsui): Reconsider the trigger condition after experimenting i.e.
  // whether it is reasonable to have it enabled when ResizeLock is enabled.
  if (touch_mode_mouse_rewriter_)
    touch_mode_mouse_rewriter_->EnableForWindow(window);

  const auto app_id = GetAppId(window);
  DCHECK(app_id);
  const bool is_fully_locked =
      window->GetProperty(ash::kArcResizeLockTypeKey) ==
      ash::ArcResizeLockType::RESIZE_DISABLED_NONTOGGLABLE;

  // The state is |ArcResizeLockState::READY| only when we enable the resize
  // lock for an app for the first time. UpdateResizeLockState() may overwrite
  // the ResizeLockState so this check must be done before it's called.
  const bool is_first_launch = pref_delegate_->GetResizeLockState(*app_id) ==
                               mojom::ArcResizeLockState::READY;
  UpdateResizeLockState(window);

  // No need to show splash screen dialog for game apps.
  if (is_first_launch && ShouldShowSplashScreenDialog(pref_delegate_) &&
      !ash::GameDashboardController::IsGameWindow(window)) {
    // UpdateResizeLockState() must be called beforehand as compat-mode button
    // must exist before showing the splash dialog because it's used as the
    // anchoring target.
    ShowSplashScreenDialog(window, is_fully_locked);
  }

  if (!is_fully_locked) {
    window->SetProperty(ash::kUnresizableSnappedSizeKey,
                        new gfx::Size(GetUnresizableSnappedWidth(window), 0));
  } else {
    window->ClearProperty(ash::kUnresizableSnappedSizeKey);
  }
}

void ArcResizeLockManager::DisableResizeLock(aura::Window* window) {
  const bool erased = resize_lock_enabled_windows_.erase(window);
  if (!erased)
    return;
  window->SetProperty(ash::kResizeShadowTypeKey,
                      ash::ResizeShadowType::kUnlock);
  // Hide shadow effect on window. ash::Shell may not exist in tests.
  if (ash::Shell::HasInstance())
    ash::Shell::Get()->resize_shadow_controller()->HideShadow(window);

  if (touch_mode_mouse_rewriter_)
    touch_mode_mouse_rewriter_->DisableForWindow(window);

  window->ClearProperty(ash::kUnresizableSnappedSizeKey);
}

void ArcResizeLockManager::UpdateResizeLockState(aura::Window* window) {
  const auto app_id = GetAppId(window);
  DCHECK(app_id);
  const auto resize_lock_type = window->GetProperty(ash::kArcResizeLockTypeKey);
  switch (resize_lock_type) {
    case ash::ArcResizeLockType::RESIZE_DISABLED_TOGGLABLE:
      pref_delegate_->SetResizeLockState(*app_id,
                                         mojom::ArcResizeLockState::ON);
      break;
    case ash::ArcResizeLockType::RESIZE_DISABLED_NONTOGGLABLE:
      pref_delegate_->SetResizeLockState(
          *app_id, mojom::ArcResizeLockState::FULLY_LOCKED);
      break;
    case ash::ArcResizeLockType::RESIZE_ENABLED_TOGGLABLE:
      pref_delegate_->SetResizeLockState(*app_id,
                                         mojom::ArcResizeLockState::OFF);
      break;
    case ash::ArcResizeLockType::NONE:
      // Maximizing an app with RESIZE_ENABLED_TOGGLABLE can lead to this case.
      // Resize lock state shouldn't be updated as the pre-maximized state
      // needs to be restored later.
      break;
  }

  // As we updated the resize lock state above, we need to update compat mode
  // button.
  compat_mode_button_controller_->Update(window);

  // Even if resize lock doesn't get enabled or disabled, we need to ensure to
  // update this as resize shadow can be updated in an intermediate state when
  // exo commites the next state.
  UpdateShadow(window);
}

void ArcResizeLockManager::UpdateShadow(aura::Window* window) {
  const auto resize_lock_type = window->GetProperty(ash::kArcResizeLockTypeKey);
  const bool resize_lock_enabled =
      resize_lock_type == ash::ArcResizeLockType::RESIZE_DISABLED_TOGGLABLE ||
      resize_lock_type == ash::ArcResizeLockType::RESIZE_DISABLED_NONTOGGLABLE;
  const auto resize_shadow_type = resize_lock_enabled
                                      ? ash::ResizeShadowType::kLock
                                      : ash::ResizeShadowType::kUnlock;
  window->SetProperty(ash::kResizeShadowTypeKey, resize_shadow_type);
  // ash::Shell may not exist in tests.
  if (ash::Shell::HasInstance()) {
    if (resize_lock_enabled) {
      ash::Shell::Get()->resize_shadow_controller()->ShowShadow(window);
    } else {
      ash::Shell::Get()->resize_shadow_controller()->HideShadow(window);
    }
  }
}

void ArcResizeLockManager::ShowSplashScreenDialog(aura::Window* window,
                                                  bool is_fully_locked) {
  WindowActivationObserver::RunOnActivated(
      window, base::BindOnce(&ArcSplashScreenDialogView::Show, window,
                             is_fully_locked));
}

// static
void ArcResizeLockManager::EnsureFactoryBuilt() {
  ArcResizeLockManagerFactory::GetInstance();
}

}  // namespace arc