chromium/ash/system/eche/eche_tray.cc

// Copyright 2022 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/system/eche/eche_tray.h"

#include <algorithm>

#include "ash/accessibility/accessibility_controller.h"
#include "ash/constants/ash_features.h"
#include "ash/constants/notifier_catalogs.h"
#include "ash/constants/tray_background_view_catalog.h"
#include "ash/keyboard/ui/keyboard_ui_controller.h"
#include "ash/public/cpp/accelerators.h"
#include "ash/public/cpp/ash_web_view.h"
#include "ash/public/cpp/ash_web_view_factory.h"
#include "ash/public/cpp/keyboard/keyboard_controller.h"
#include "ash/public/cpp/shell_window_ids.h"
#include "ash/public/cpp/system/toast_data.h"
#include "ash/public/cpp/system/toast_manager.h"
#include "ash/public/cpp/tablet_mode_observer.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/root_window_controller.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shelf/shelf.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/style/ash_color_id.h"
#include "ash/style/icon_button.h"
#include "ash/style/typography.h"
#include "ash/system/eche/eche_icon_loading_indicator_view.h"
#include "ash/system/phonehub/phone_hub_tray.h"
#include "ash/system/phonehub/ui_constants.h"
#include "ash/system/status_area_widget.h"
#include "ash/system/tray/tray_bubble_wrapper.h"
#include "ash/system/tray/tray_container.h"
#include "ash/system/tray/tray_popup_utils.h"
#include "ash/system/tray/tray_utils.h"
#include "ash/webui/eche_app_ui/mojom/eche_app.mojom-shared.h"
#include "ash/webui/eche_app_ui/mojom/eche_app.mojom.h"
#include "ash/wm/window_state.h"
#include "base/functional/bind.h"
#include "base/functional/callback_forward.h"
#include "base/metrics/histogram_functions.h"
#include "base/notreached.h"
#include "base/time/default_tick_clock.h"
#include "base/time/time.h"
#include "chromeos/ash/components/multidevice/logging/logging.h"
#include "components/account_id/account_id.h"
#include "components/session_manager/session_manager_types.h"
#include "components/vector_icons/vector_icons.h"
#include "ui/base/accelerators/accelerator.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/models/image_model.h"
#include "ui/chromeos/styles/cros_tokens_color_mappings.h"
#include "ui/compositor/layer.h"
#include "ui/events/event.h"
#include "ui/events/event_constants.h"
#include "ui/events/event_target.h"
#include "ui/events/keycodes/keyboard_codes_posix.h"
#include "ui/events/types/event_type.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/geometry/point.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/size.h"
#include "ui/gfx/geometry/vector2d.h"
#include "ui/gfx/image/image.h"
#include "ui/gfx/image/image_skia_operations.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/gfx/text_constants.h"
#include "ui/gfx/vector_icon_types.h"
#include "ui/strings/grit/ui_strings.h"
#include "ui/views/border.h"
#include "ui/views/controls/button/image_button.h"
#include "ui/views/controls/button/image_button_factory.h"
#include "ui/views/controls/highlight_path_generator.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/layout/flex_layout.h"
#include "ui/views/layout/layout_types.h"
#include "ui/views/view.h"
#include "ui/views/views_delegate.h"
#include "url/gurl.h"

// Uncomment the following line to make a fake
// bubble for local testing only.
// #define FAKE_BUBBLE_FOR_DEBUG

namespace ash {

namespace {

const char kEchePrewarmConnectionUrl[] = "chrome://eche-app";

// The icon size should be smaller than the tray item size to avoid the icon
// padding becoming negative.
constexpr int kIconSize = 24;

// This is how much the icon shrinks to give space for the spinner to go
// around it.
constexpr int kIconShrinkSizeForSpinner = 4;

constexpr int kHeaderHeight = 40;
constexpr int kHeaderHorizontalInteriorMargins = 0;
constexpr auto kHeaderDefaultSpacing = gfx::Insets::VH(0, 6);

constexpr auto kBubblePadding = gfx::Insets::VH(8, 8);

constexpr float kDefaultAspectRatio = 16.0 / 9.0f;
constexpr gfx::Size kDefaultBubbleSize(360, 360 * kDefaultAspectRatio);

// Max percentage of the screen height that can be covered by the eche bubble.
constexpr float kMaxHeightPercentage = 0.85;

// Unload timeout to close Eche Bubble in case error from Ech web during closing
constexpr base::TimeDelta kUnloadTimeoutDuration = base::Milliseconds(500);

// Timeout for initializer connection attempts.
constexpr base::TimeDelta kInitializerTimeout = base::Seconds(6);

// The ID for the "Tablet mode not supported" toast.
constexpr char kEcheTrayTabletModeNotSupportedId[] =
    "eche_tray_toast_ids.tablet_mode_not_supported";

// AcceleratorsActions which should be handled by the AcceleratorController, not
// the eche tray.
constexpr AcceleratorAction kLocallyProcessedAcceleratorActions[] = {
    AcceleratorAction::kOpenFeedbackPage,           // Shift + Alt + I
    AcceleratorAction::kExit,                       // Shift + Ctrl + Q
    AcceleratorAction::kShowShortcutViewer,         // Ctrl + Alt + /
    AcceleratorAction::kToggleCapsLock,             // Alt + Search
    AcceleratorAction::kNewWindow,                  // Ctrl + N
    AcceleratorAction::kNewIncognitoWindow,         // Shift + Ctrl + N
    AcceleratorAction::kNewTab,                     // Ctrl + T
    AcceleratorAction::kOpenFileManager,            // Shift + Alt + M
    AcceleratorAction::kLaunchApp0,                 // Alt + 1
    AcceleratorAction::kLaunchApp1,                 // Alt + 2
    AcceleratorAction::kLaunchApp2,                 // Alt + 3
    AcceleratorAction::kLaunchApp3,                 // Alt + 4
    AcceleratorAction::kLaunchApp4,                 // Alt + 5
    AcceleratorAction::kLaunchApp5,                 // Alt + 6
    AcceleratorAction::kLaunchApp6,                 // Alt + 7
    AcceleratorAction::kLaunchApp7,                 // Alt + 8
    AcceleratorAction::kLaunchLastApp,              // Alt + 9
    AcceleratorAction::kToggleMessageCenterBubble,  // Shift + Alt + N
    AcceleratorAction::kScaleUiUp,                  // Shift + Ctrl + "+"
    AcceleratorAction::kScaleUiDown,                // Shift + Ctrl + "-"
    AcceleratorAction::kScaleUiReset,               // Shift + Ctrl + 0
    AcceleratorAction::kRotateScreen,               // Shift + Ctrl + Refresh
    AcceleratorAction::kToggleSpokenFeedback,       // Ctrl + Alt + Z
    AcceleratorAction::kFocusShelf,                 // Shift + Alt + L
    AcceleratorAction::kFocusNextPane,              // Ctrl + Back
    AcceleratorAction::kFocusPreviousPane,          // Ctrl + Forward
    AcceleratorAction::kToggleAppList               // Launcher(Search)
};

// Creates a button with the given callback, icon, and tooltip text.
// `message_id` is the resource id of the tooltip text of the icon.
std::unique_ptr<views::Button> CreateButton(
    views::Button::PressedCallback callback,
    const gfx::VectorIcon& icon,
    int message_id) {
  auto button = views::CreateVectorImageButton(std::move(callback));

  views::SetImageFromVectorIconWithColorId(
      button.get(), icon,
      static_cast<ui::ColorId>(cros_tokens::kCrosSysOnSurface),
      static_cast<ui::ColorId>(cros_tokens::kButtonIconColorPrimaryDisabled));
  button->SetTooltipText(l10n_util::GetStringUTF16(message_id));
  button->SizeToPreferredSize();

  views::InstallCircleHighlightPathGenerator(button.get());

  return button;
}

std::unique_ptr<AshWebView> CreateWebview() {
  AshWebView::InitParams params;
  params.can_record_media = true;
  return AshWebViewFactory::Get()->Create(params);
}

void ConfigureLabelText(views::Label* title) {
  title->SetMultiLine(false);
  title->SetAllowCharacterBreak(true);
  title->SetProperty(
      views::kFlexBehaviorKey,
      views::FlexSpecification(views::MinimumFlexSizeRule::kScaleToZero,
                               views::MaximumFlexSizeRule::kUnbounded,
                               /*adjust_height_for_width =*/true)
          .WithWeight(1));
  title->SetHorizontalAlignment(gfx::ALIGN_CENTER);
  title->SetEnabledColorId(cros_tokens::kCrosSysOnSurface);
  TypographyProvider::Get()->StyleLabel(ash::TypographyToken::kCrosHeadline1,
                                        *title);
}

}  // namespace

EcheTray::EventInterceptor::EventInterceptor(EcheTray* eche_tray)
    : eche_tray_(eche_tray) {}
EcheTray::EventInterceptor::~EventInterceptor() = default;

void EcheTray::EventInterceptor::OnKeyEvent(ui::KeyEvent* event) {
  if (eche_tray_->ProcessAcceleratorKeys(event)) {
    event->StopPropagation();
    return;
  }
}

EcheTray::EcheTray(Shelf* shelf)
    : TrayBackgroundView(shelf, TrayBackgroundViewCatalogName::kEche),
      icon_(
          tray_container()->AddChildView(std::make_unique<views::ImageView>())),
      event_interceptor_(std::make_unique<EventInterceptor>(this)) {
  SetCallback(
      base::BindRepeating(&EcheTray::OnButtonPressed, base::Unretained(this)));

  const int icon_padding = (kTrayItemSize - kIconSize) / 2;

  icon_->SetBorder(
      views::CreateEmptyBorder(gfx::Insets::VH(icon_padding, icon_padding)));

  // Observers setup
  // Note: `ScreenLayoutObserver` starts observing at its constructor.
  observed_session_.Observe(Shell::Get()->session_controller());
  icon_->SetTooltipText(GetAccessibleNameForTray());
  UpdateTrayItemColor(is_active());

  shelf_observation_.Observe(shelf);
  shell_observer_.Observe(Shell::Get());
  keyboard_observation_.Observe(keyboard::KeyboardUIController::Get());
}

EcheTray::~EcheTray() {
  if (bubble_) {
    bubble_->bubble_view()->ResetDelegate();
  }
  if (features::IsEcheNetworkConnectionStateEnabled() &&
      eche_connection_status_handler_) {
    eche_connection_status_handler_->RemoveObserver(this);
  }
}

bool EcheTray::IsInitialized() const {
  return GetBubbleWidget() != nullptr;
}

void EcheTray::ClickedOutsideBubble(const ui::LocatedEvent& event) {
  //  Do nothing
}

void EcheTray::UpdateTrayItemColor(bool is_active) {
  icon_->SetImage(ui::ImageModel::FromVectorIcon(
      kPhoneHubPhoneIcon, is_active
                              ? cros_tokens::kCrosSysSystemOnPrimaryContainer
                              : cros_tokens::kCrosSysOnSurface));
}

std::u16string EcheTray::GetAccessibleNameForTray() {
  // TODO(nayebi): Change this based on the final model of interaction
  // between phone hub and Eche.
  return l10n_util::GetStringUTF16(IDS_ASH_PHONE_HUB_TRAY_ACCESSIBLE_NAME);
}

void EcheTray::HandleLocaleChange() {
  icon_->SetTooltipText(GetAccessibleNameForTray());
}

void EcheTray::HideBubbleWithView(const TrayBubbleView* bubble_view) {
  if (bubble_->bubble_view() == bubble_view)
    HideBubble();
}

void EcheTray::AnchorUpdated() {
  if (bubble_)
    bubble_->bubble_view()->UpdateBubble();
}

void EcheTray::Initialize() {
  TrayBackgroundView::Initialize();

  // By default the icon is not visible until Eche notification is clicked on.
  bool visibility = false;
#ifdef FAKE_BUBBLE_FOR_DEBUG
  visibility = true;
#endif
  SetVisiblePreferred(visibility);
}

void EcheTray::CloseBubbleInternal() {
  if (bubble_)
    HideBubble();
}

void EcheTray::ShowBubble() {
#ifdef FAKE_BUBBLE_FOR_DEBUG
  LoadBubble(GURL("http://google.com"), std::move(gfx::Image()),
             u"visible_name");
  return;
#endif

  if (!bubble_)
    return;
  SetIconVisibility(true);
  StopLoadingAnimation();

  bubble_->GetBubbleWidget()->Show();
  bubble_->GetBubbleWidget()->Activate();

  bubble_->bubble_view()->SetVisible(true);
  // Since this tray already initialize the bubble before showing it in
  // `LoadBubble()`, we need to call `NotifyTrayBubbleOpen()` here.
  bubble_->bubble_view()->NotifyTrayBubbleOpen();

  SetIsActive(true);
  web_view_->GetInitiallyFocusedView()->RequestFocus();

  aura::Window* window = bubble_->GetBubbleWidget()->GetNativeWindow();
  if (!window)
    return;
  window = window->GetToplevelWindow();
  WindowState* window_state = WindowState::Get(window);
  // We need this as `WorkspaceLayoutManager` conflicts with our resizing.
  // See b/229111865#comment5
  window_state->set_ignore_keyboard_bounds_change(true);
  bubble_->GetBubbleWidget()->GetNativeWindow()->AddPreTargetHandler(
      event_interceptor_.get());
  shelf()->UpdateAutoHideState();
  if (bubble_shown_callback_) {
    bubble_shown_callback_.Run(web_view_);
  }
}

TrayBubbleView* EcheTray::GetBubbleView() {
  return bubble_ ? bubble_->bubble_view() : nullptr;
}

views::Widget* EcheTray::GetBubbleWidget() const {
  return bubble_ ? bubble_->GetBubbleWidget() : nullptr;
}

void EcheTray::OnVirtualKeyboardVisibilityChanged() {
  OnKeyboardVisibilityChanged(KeyboardController::Get()->IsKeyboardVisible());
  TrayBackgroundView::OnVirtualKeyboardVisibilityChanged();
}

bool EcheTray::CacheBubbleViewForHide() const {
  return true;
}

std::u16string EcheTray::GetAccessibleNameForBubble() {
  return GetAccessibleNameForTray();
}

bool EcheTray::ShouldEnableExtraKeyboardAccessibility() {
  return Shell::Get()->accessibility_controller()->spoken_feedback().enabled();
}

void EcheTray::HideBubble(const TrayBubbleView* bubble_view) {
  HideBubbleWithView(bubble_view);
}

void EcheTray::OnStreamStatusChanged(eche_app::mojom::StreamStatus status) {
  switch (status) {
    case eche_app::mojom::StreamStatus::kStreamStatusStarted:
      // Reset the timestamp when the streaming is started.
      init_stream_timestamp_.reset();
      is_stream_started_ = true;
      ShowBubble();
      break;
    case eche_app::mojom::StreamStatus::kStreamStatusStopped:
      is_stream_started_ = false;
      PurgeAndClose();
      break;
    case eche_app::mojom::StreamStatus::kStreamStatusInitializing:
      is_stream_started_ = false;
      break;
    case eche_app::mojom::StreamStatus::kStreamStatusUnknown:
      PA_LOG(WARNING) << "Unexpected stream status";
      is_stream_started_ = false;
      break;
  }
}

void EcheTray::OnLockStateChanged(bool locked) {
  if (bubble_ && locked)
    PurgeAndClose();
}

void EcheTray::OnKeyboardUIDestroyed() {
  if (!IsBubbleVisible())
    return;
  UpdateEcheSizeAndBubbleBounds();
}

void EcheTray::OnKeyboardHidden(bool is_temporary_hide) {
  if (!IsBubbleVisible())
    return;
  UpdateEcheSizeAndBubbleBounds();
}

void EcheTray::OnConnectionStatusChanged(
    eche_app::mojom::ConnectionStatus connection_status) {
  if (!features::IsEcheNetworkConnectionStateEnabled() ||
      !initializer_webview_) {
    return;
  }

  switch (connection_status) {
    case eche_app::mojom::ConnectionStatus::kConnectionStatusConnecting:
      break;

    case eche_app::mojom::ConnectionStatus::kConnectionStatusConnected:
      PA_LOG(INFO) << "Connection successful, updating UI to connected.";
      eche_connection_status_handler_->SetConnectionStatusForUi(
          connection_status);
      has_reported_initializer_result_ = true;
      base::UmaHistogramBoolean("Eche.NetworkCheck.Result", true);
      StartGracefulCloseInitializer();
      break;
    case eche_app::mojom::ConnectionStatus::kConnectionStatusFailed:
      PA_LOG(WARNING) << "Connection failed, updating UI to error state.";
      eche_connection_status_handler_->SetConnectionStatusForUi(
          connection_status);
      base::UmaHistogramBoolean("Eche.NetworkCheck.Result", false);
      has_reported_initializer_result_ = true;
      StartGracefulCloseInitializer();
      break;
    case eche_app::mojom::ConnectionStatus::kConnectionStatusDisconnected:
      // If we've timed out or been disconnected before a success/failure has
      // come in, report failure, unless we intentionally disconnected in
      // preparation for an app stream launch.
      if (!has_reported_initializer_result_ && !on_initializer_closed_) {
        PA_LOG(WARNING)
            << "Disconnected without result, updating UI to error state.";
        base::UmaHistogramBoolean("Eche.NetworkCheck.Result", false);
        eche_connection_status_handler_->SetConnectionStatusForUi(
            eche_app::mojom::ConnectionStatus::kConnectionStatusFailed);
      }
      // If the status is changed kConnectionStatusDisconnected before the
      // timeout, manually cancel the timeout task. Also notify that the
      // connection has been closed so that each component can clean up.
      if (initializer_timeout_) {
        initializer_timeout_.reset();
        eche_connection_status_handler_->NotifyConnectionClosed();
      }
      initializer_webview_.reset();
      break;
  }
}

void EcheTray::OnRequestBackgroundConnectionAttempt() {
  if (!features::IsEcheNetworkConnectionStateEnabled() || web_view_) {
    return;
  }
  has_reported_initializer_result_ = false;
  initializer_webview_ = CreateWebview();
  initializer_webview_->Navigate(GURL(kEchePrewarmConnectionUrl));
  initializer_timeout_ = std::make_unique<base::DelayTimer>(
      FROM_HERE, kInitializerTimeout, this,
      &EcheTray::OnBackgroundConnectionTimeout);
  initializer_timeout_->Reset();  // Starts the timer.
  SetIconVisibility(false);
}

void EcheTray::OnStatusAreaAnchoredBubbleVisibilityChanged(
    TrayBubbleView* tray_bubble,
    bool visible) {
  // We only care about "other" bubbles being shown.
  if (!bubble_ || tray_bubble == GetBubbleView()) {
    return;
  }

  // Another bubble has become visible, so minimize this one.
  if (visible && IsBubbleVisible()) {
    HideBubble();
  }
}

void EcheTray::CloseInitializer() {
  initializer_webview_.reset();
  if (on_initializer_closed_) {
    std::move(on_initializer_closed_).Run();
  }
}

void EcheTray::OnBackgroundConnectionTimeout() {
  if (!initializer_webview_ || web_view_) {
    return;
  }

  // Notify that the connection attempt failed reset the connection status for
  // timeouts, this happens automatically for other failures.
  eche_connection_status_handler_->SetConnectionStatusForUi(
      eche_app::mojom::ConnectionStatus::kConnectionStatusFailed);
  StartGracefulCloseInitializer();
}

void EcheTray::StartGracefulCloseInitializer() {
  if (!initializer_webview_) {
    return;
  }

  initializer_timeout_.reset();
  eche_connection_status_handler_->NotifyRequestCloseConnection();
  unload_timer_ = std::make_unique<base::DelayTimer>(
      FROM_HERE, kUnloadTimeoutDuration, this, &EcheTray::CloseInitializer);
  unload_timer_->Reset();  // Starts the timer.
}

void EcheTray::OnButtonPressed() {
  // The `bubble_` is cached, so don't check for existence (which is the base
  // TrayBackgroundView implementation), check for visibility to decide on
  // whether to show or hide.
  if (IsBubbleVisible()) {
    HideBubble();
    return;
  }
  ShowBubble();
}

void EcheTray::SetUrl(const GURL& url) {
  if (web_view_ && url_ != url)
    web_view_->Navigate(url);
  url_ = url;
}

void EcheTray::SetIcon(const gfx::Image& icon,
                       const std::u16string& tooltip_text) {
  views::ImageButton* icon_view = GetIcon();
  if (icon_view) {
    icon_view->SetImageModel(
        views::ImageButton::STATE_NORMAL,
        ui::ImageModel::FromImageSkia(
            gfx::ImageSkiaOperations::CreateResizedImage(
                icon.AsImageSkia(), skia::ImageOperations::RESIZE_BEST,
                gfx::Size(kIconSize, kIconSize))));
    icon_view->SetTooltipText(tooltip_text);
    SetIconVisibility(true);
  }
}

bool EcheTray::LoadBubble(
    const GURL& url,
    const gfx::Image& icon,
    const std::u16string& visible_name,
    const std::u16string& phone_name,
    eche_app::mojom::ConnectionStatus last_connection_status,
    eche_app::mojom::AppStreamLaunchEntryPoint entry_point) {
  if (Shell::Get()->IsInTabletMode()) {
    ash::ToastManager::Get()->Show(ash::ToastData(
        kEcheTrayTabletModeNotSupportedId,
        ash::ToastCatalogName::kEcheTrayTabletModeNotSupported,
        l10n_util::GetStringUTF16(IDS_ASH_ECHE_TOAST_TABLET_MODE_NOT_SUPPORTED),
        ash::ToastData::kDefaultToastDuration,
        /*visible_on_lock_screen=*/false));
    PA_LOG(WARNING) << "Eche load failed due to tablet mode.";
    base::UmaHistogramEnumeration(
        "Eche.StreamEvent.ConnectionFail",
        EcheTray::ConnectionFailReason::kConnectionFailInTabletMode);
    return false;
  }
  SetUrl(url);
  SetIcon(icon, /*tooltip_text=*/visible_name);
  // If the bubble is already initialized, setting the icon and url was enough
  // to navigate the bubble to the new address.
  if (IsInitialized()) {
    ShowBubble();
    return true;
  }
  InitBubble(phone_name, last_connection_status, entry_point);
  StartLoadingAnimation();
  auto* phone_hub_tray = GetPhoneHubTray();
  if (phone_hub_tray) {
    phone_hub_tray->SetEcheIconActivationCallback(base::BindRepeating(
        &EcheTray::OnButtonPressed, base::Unretained(this)));
  }
  // Hide bubble first until the streaming is ready.
  HideBubble();
  return true;
}

void EcheTray::PurgeAndClose() {
  StopLoadingAnimation();
  SetIconVisibility(false);
  is_landscape_ = false;

  if (!bubble_)
    return;

  auto* bubble_view = bubble_->GetBubbleView();
  if (bubble_view)
    bubble_view->ResetDelegate();

  SetIsActive(false);
  SetVisiblePreferred(false);
  web_view_ = nullptr;
  close_button_ = nullptr;
  minimize_button_ = nullptr;
  arrow_back_button_ = nullptr;
  unload_timer_.reset();
  bubble_.reset();
  init_stream_timestamp_.reset();
}

void EcheTray::SetGracefulCloseCallback(
    GracefulCloseCallback graceful_close_callback) {
  if (!graceful_close_callback)
    return;
  graceful_close_callback_ = std::move(graceful_close_callback);
}

void EcheTray::SetGracefulGoBackCallback(
    GracefulGoBackCallback graceful_go_back_callback) {
  if (!graceful_go_back_callback)
    return;
  graceful_go_back_callback_ = std::move(graceful_go_back_callback);
}

void EcheTray::SetBubbleShownCallback(
    BubbleShownCallback bubble_shown_callback) {
  if (!bubble_shown_callback) {
    return;
  }
  bubble_shown_callback_ = std::move(bubble_shown_callback);
}

void EcheTray::HideBubble() {
  if (!bubble_)
    return;
  bubble_->GetBubbleWidget()->GetNativeWindow()->RemovePreTargetHandler(
      event_interceptor_.get());
  SetIsActive(false);
  bubble_->bubble_view()->SetVisible(false);

  // Since this tray just hide and do not destroy the bubble when closing, we
  // need to call `NotifyTrayBubbleClosed()`.
  bubble_->bubble_view()->NotifyTrayBubbleClosed();

  bubble_->GetBubbleWidget()->Deactivate();
  bubble_->GetBubbleWidget()->Hide();
  shelf()->UpdateAutoHideState();
}

void EcheTray::InitBubble(
    const std::u16string& phone_name,
    eche_app::mojom::ConnectionStatus last_connection_status,
    eche_app::mojom::AppStreamLaunchEntryPoint entry_point) {
  // We only support a single connection between the phone and chromebook, if
  // there's an existing background connection it must first be disconnected
  // before we can continue with the app stream initialization.
  // TODO(b/283880725) re-use the existing connection instead of terminating it
  // and starting a new one.
  if (initializer_webview_) {
    PA_LOG(INFO)
        << "Active background connection must be terminated prior to launching "
           "app.  Saving launch details and will retry once ready.";
    on_initializer_closed_ =
        base::BindOnce(&EcheTray::InitBubble, base::Unretained(this),
                       phone_name, last_connection_status, entry_point);
    StartGracefulCloseInitializer();
    return;
  }

  if (features::IsEcheNetworkConnectionStateEnabled() &&
      last_connection_status !=
          eche_app::mojom::ConnectionStatus::kConnectionStatusConnected &&
      entry_point == eche_app::mojom::AppStreamLaunchEntryPoint::NOTIFICATION) {
    base::UmaHistogramEnumeration(
        "Eche.StreamEvent.FromNotification.PreviousNetworkCheckFailed.Result",
        eche_app::mojom::StreamStatus::kStreamStatusInitializing);
  } else {
    base::UmaHistogramEnumeration(
        "Eche.StreamEvent",
        eche_app::mojom::StreamStatus::kStreamStatusInitializing);
    switch (entry_point) {
      case eche_app::mojom::AppStreamLaunchEntryPoint::APPS_LIST:
        base::UmaHistogramEnumeration(
            "Eche.StreamEvent.FromLauncher",
            eche_app::mojom::StreamStatus::kStreamStatusInitializing);
        break;
      case eche_app::mojom::AppStreamLaunchEntryPoint::NOTIFICATION:
        base::UmaHistogramEnumeration(
            "Eche.StreamEvent.FromNotification",
            eche_app::mojom::StreamStatus::kStreamStatusInitializing);
        break;
      case eche_app::mojom::AppStreamLaunchEntryPoint::RECENT_APPS:
        base::UmaHistogramEnumeration(
            "Eche.StreamEvent.FromRecentApps",
            eche_app::mojom::StreamStatus::kStreamStatusInitializing);
        break;
      case eche_app::mojom::AppStreamLaunchEntryPoint::UNKNOWN:
        NOTREACHED();
    }
  }
  init_stream_timestamp_ = base::TimeTicks::Now();
  TrayBubbleView::InitParams init_params = CreateInitParamsForTrayBubble(
      /*tray=*/this, /*anchor_to_shelf_corner=*/true);

  // Note: The container id must be smaller than `kShellWindowId_ShelfContainer`
  // in order to let the notifications be shown on top of the eche window.
  init_params.parent_window = Shell::GetContainer(
      tray_container()->GetWidget()->GetNativeWindow()->GetRootWindow(),
      kShellWindowId_AlwaysOnTopContainer);
  const gfx::Size eche_size = CalculateSizeForEche();
  init_params.preferred_width = eche_size.width();
  init_params.close_on_deactivate = false;
  init_params.reroute_event_handler = false;

  phone_name_ = phone_name;

  auto bubble_view = std::make_unique<TrayBubbleView>(init_params);
  bubble_view->SetCanActivate(true);
  bubble_view->SetBorder(views::CreateEmptyBorder(kBubblePadding));

  header_view_ = bubble_view->AddChildView(CreateBubbleHeaderView(phone_name));

  // We need the header be always visible with the same size.
  bubble_view->box_layout()->SetFlexForView(header_view_, 0, true);
  bubble_view->box_layout()->set_inside_border_insets(kBubblePadding);

  // TODO(b/271478560): Re-use initializer_webview_ when available, once support
  // launching apps on prewarmed connection is available.
  auto web_view = CreateWebview();
  web_view->SetPreferredSize(eche_size);
  if (!url_.is_empty())
    web_view->Navigate(url_);
  web_view_ = bubble_view->AddChildView(std::move(web_view));

  bubble_ = std::make_unique<TrayBubbleWrapper>(this,
                                                /*event_handling=*/false);
  bubble_->ShowBubble(std::move(bubble_view));
  SetIsActive(true);
  bubble_->GetBubbleView()->UpdateBubble();
}

void EcheTray::StartGracefulClose() {
  if (init_stream_timestamp_.has_value()) {
    base::UmaHistogramLongTimes100(
        "Eche.StreamEvent.Duration.FromInitializeToClose",
        base::TimeTicks::Now() - *init_stream_timestamp_);
    init_stream_timestamp_.reset();
  }

  // If there's an initializer session running it should also be shutdown.
  StartGracefulCloseInitializer();

  if (!graceful_close_callback_) {
    PurgeAndClose();
    return;
  }
  HideBubble();
  std::move(graceful_close_callback_).Run();
  // Graceful close will let Eche Web to close connection release then notify
  // back to native code to close window. In case there is any exception happens
  // in js layer, start a timer to force close widget in case unload can't be
  // finished.
  if (!unload_timer_) {
    unload_timer_ = std::make_unique<base::DelayTimer>(
        FROM_HERE, kUnloadTimeoutDuration, this, &EcheTray::PurgeAndClose);
    unload_timer_->Reset();
  }
}

gfx::Size EcheTray::CalculateSizeForEche() const {
  const gfx::Rect work_area_bounds =
      display::Screen::GetScreen()
          ->GetDisplayNearestWindow(
              tray_container()->GetWidget()->GetNativeWindow())
          .work_area();
  float height_scale =
      (static_cast<float>(work_area_bounds.height()) * kMaxHeightPercentage) /
      kDefaultBubbleSize.height();
  height_scale = std::min(height_scale, 1.0f);
  gfx::Size size = gfx::ScaleToFlooredSize(kDefaultBubbleSize, height_scale);

  // TODO(b/258306301): Verify the correct sizing for Landscape
  if (is_landscape_) {
    size = gfx::Size(size.height(), size.width());
  }

  return size;
}

void EcheTray::OnArrowBackActivated() {
  if (web_view_) {
    // TODO(b/228909439): Call `web_view_` GoBack with
    // `graceful_go_back_callback_` together to avoid the back button not
    // working when the stream action GoBack isn’t ready in web content yet.
    // Remove this when the stream action GoBack is ready in web content.
    web_view_->GoBack();

    if (graceful_go_back_callback_)
      graceful_go_back_callback_.Run();
  }
}

std::unique_ptr<views::View> EcheTray::CreateBubbleHeaderView(
    const std::u16string& phone_name) {
  auto header = std::make_unique<views::View>();
  header->SetLayoutManager(std::make_unique<views::FlexLayout>())
      ->SetInteriorMargin(gfx::Insets::VH(0, kHeaderHorizontalInteriorMargins))
      .SetCollapseMargins(false)
      .SetMinimumCrossAxisSize(kHeaderHeight)
      .SetDefault(views::kMarginsKey, kHeaderDefaultSpacing)
      .SetCrossAxisAlignment(views::LayoutAlignment::kCenter);

  // Add arrowback button
  arrow_back_button_ = header->AddChildView(
      CreateButton(base::BindRepeating(&EcheTray::OnArrowBackActivated,
                                       weak_factory_.GetWeakPtr()),
                   kEcheArrowBackIcon, IDS_APP_ACCNAME_BACK));

  views::Label* title = header->AddChildView(std::make_unique<views::Label>(
      l10n_util::GetStringFUTF16(ID_ASH_ECHE_APP_STREAMING_BUBBLE_TITLE,
                                 phone_name),
      views::style::CONTEXT_DIALOG_TITLE, views::style::STYLE_PRIMARY,
      gfx::DirectionalityMode::DIRECTIONALITY_FROM_TEXT));
  ConfigureLabelText(title);

  // Add minimize button
  minimize_button_ = header->AddChildView(CreateButton(
      base::BindRepeating(&EcheTray::CloseBubble, weak_factory_.GetWeakPtr()),
      kEcheMinimizeIcon, IDS_APP_ACCNAME_MINIMIZE));

  // Add close button
  close_button_ = header->AddChildView(
      CreateButton(base::BindRepeating(&EcheTray::StartGracefulClose,
                                       weak_factory_.GetWeakPtr()),
                   kEcheCloseIcon, IDS_APP_ACCNAME_CLOSE));

  return header;
}

views::Button* EcheTray::GetMinimizeButtonForTesting() const {
  return minimize_button_;
}

views::Button* EcheTray::GetCloseButtonForTesting() const {
  return close_button_;
}

views::Button* EcheTray::GetArrowBackButtonForTesting() const {
  return arrow_back_button_;
}

views::ImageButton* EcheTray::GetIcon() {
  PhoneHubTray* phone_hub_tray = GetPhoneHubTray();
  if (!phone_hub_tray)
    return nullptr;
  return phone_hub_tray->eche_icon_view();
}

void EcheTray::ResizeIcon(int offset_dip) {
  views::ImageButton* icon_view = GetIcon();
  if (icon_view) {
    auto icon = icon_view->GetImage(views::ImageButton::STATE_NORMAL);
    icon_view->SetImageModel(
        views::ImageButton::STATE_NORMAL,
        ui::ImageModel::FromImageSkia(
            gfx::ImageSkiaOperations::CreateResizedImage(
                icon, skia::ImageOperations::RESIZE_BEST,
                gfx::Size(kIconSize - offset_dip, kIconSize - offset_dip))));
    GetPhoneHubTray()->tray_container()->UpdateLayout();
  }
}

void EcheTray::StopLoadingAnimation() {
  ResizeIcon(0);
  auto* loading_indicator = GetLoadingIndicator();
  if (loading_indicator && loading_indicator->GetAnimating()) {
    loading_indicator->SetAnimating(false);
  }
}

void EcheTray::StartLoadingAnimation() {
  ResizeIcon(kIconShrinkSizeForSpinner);
  auto* loading_indicator = GetLoadingIndicator();
  if (loading_indicator) {
    loading_indicator->SetAnimating(true);
  }
}

void EcheTray::SetIconVisibility(bool visibility) {
  auto* icon = GetIcon();
  if (!icon)
    return;
  icon->SetVisible(visibility);
  GetPhoneHubTray()->tray_container()->UpdateLayout();
}

PhoneHubTray* EcheTray::GetPhoneHubTray() {
  return shelf()->GetStatusAreaWidget()->phone_hub_tray();
}

EcheIconLoadingIndicatorView* EcheTray::GetLoadingIndicator() {
  PhoneHubTray* phone_hub_tray = GetPhoneHubTray();
  if (!phone_hub_tray)
    return nullptr;
  return phone_hub_tray->eche_loading_indicator();
}

void EcheTray::UpdateEcheSizeAndBubbleBounds() {
  if (!bubble_ || !bubble_->GetBubbleView())
    return;
  gfx::Size eche_size = CalculateSizeForEche();
  bubble_->GetBubbleView()->SetPreferredWidth(eche_size.width());
  web_view_->SetPreferredSize(eche_size);
  bubble_->GetBubbleView()->ChangeAnchorRect(
      shelf()->GetSystemTrayAnchorRect());
}

void EcheTray::OnDidApplyDisplayChanges() {
  UpdateEcheSizeAndBubbleBounds();
}

void EcheTray::OnAutoHideStateChanged(ShelfAutoHideState state) {
  UpdateEcheSizeAndBubbleBounds();
}

void EcheTray::OnDisplayTabletStateChanged(display::TabletState state) {
  switch (state) {
    case display::TabletState::kEnteringTabletMode:
    case display::TabletState::kExitingTabletMode:
      break;
    case display::TabletState::kInTabletMode:
      OnTabletModeStarted();
      break;
    case display::TabletState::kInClamshellMode:
      UpdateEcheSizeAndBubbleBounds();
      break;
  }
}

void EcheTray::OnTabletModeStarted() {
  if (!IsBubbleVisible())
    return;

  // Device changes to tablet mode but the streaming has not started yet, we
  // should log as connection failure.
  if (!is_stream_started_) {
    base::UmaHistogramEnumeration(
        "Eche.StreamEvent.ConnectionFail",
        EcheTray::ConnectionFailReason::kConnectionFailInTabletMode);
  }
  ash::ToastManager::Get()->Show(ash::ToastData(
      kEcheTrayTabletModeNotSupportedId,
      ash::ToastCatalogName::kEcheTrayTabletModeNotSupported,
      l10n_util::GetStringUTF16(IDS_ASH_ECHE_TOAST_TABLET_MODE_NOT_SUPPORTED),
      ash::ToastData::kDefaultToastDuration,
      /*visible_on_lock_screen=*/false));
  PurgeAndClose();
}

void EcheTray::OnShelfAlignmentChanged(aura::Window* root_window,
                                       ShelfAlignment old_alignment) {
  UpdateEcheSizeAndBubbleBounds();
}

void EcheTray::OnStreamOrientationChanged(bool is_landscape) {
  if (is_landscape_ == is_landscape) {
    return;
  }

  is_landscape_ = is_landscape;
  UpdateEcheSizeAndBubbleBounds();
}

// TODO(b/234848974): Try to use View::AddAccelerator for the bubble view
// and then add the handler in View::AcceleratorPressed.
bool EcheTray::ProcessAcceleratorKeys(ui::KeyEvent* event) {
  ui::Accelerator accelerator(*event);

  auto* accelerator_controller = AcceleratorController::Get();

  // Process minimize action
  // Please note that the bubble is not a normal window and it has a special
  // minimize behavior that is closer to hide than real minimize.
  //
  // TODO(https://crbug/1338650): See if we can just leave this to be handled
  // upper in the chain and perform the minimize by reacting to
  // ToggleMinimized().
  if (accelerator_controller->DoesAcceleratorMatchAction(
          accelerator, AcceleratorAction::kWindowMinimize)) {
    CloseBubble();
    return true;
  }

  for (AcceleratorAction accelerator_action :
       kLocallyProcessedAcceleratorActions) {
    if (accelerator_controller->DoesAcceleratorMatchAction(
            accelerator, accelerator_action)) {
      views::ViewsDelegate::GetInstance()->ProcessAcceleratorWhileMenuShowing(
          accelerator);
      event->StopPropagation();
      return true;
    }
  }

  const ui::KeyboardCode key_code = event->key_code();
  const bool is_only_control_down = ui::Accelerator::MaskOutKeyEventFlags(
                                        event->flags()) == ui::EF_CONTROL_DOWN;
  const bool any_modifier_pressed =
      ui::Accelerator::MaskOutKeyEventFlags(event->flags());

  if (event->type() != ui::EventType::kKeyPressed) {
    return false;
  }

  switch (key_code) {
    case ui::VKEY_W:
      if (!is_only_control_down)
        return false;
      // Please note that ctrl+w does not have a global accelerator action
      // similar to AcceleratorAction::kWindowMinimize that was used above.
      //
      // TODO(https://crbug/1338650): See if we can just leave this to be
      // handled upper in the chain.
      StartGracefulClose();
      return true;
    case ui::VKEY_ESCAPE:
      StartGracefulClose();
      return true;
    case ui::VKEY_BROWSER_BACK:
      if (any_modifier_pressed)
        return false;
      OnArrowBackActivated();
      return true;

    default:
      return false;
  }
}

bool EcheTray::IsBubbleVisible() {
  return bubble_ && bubble_->GetBubbleView() &&
         bubble_->GetBubbleView()->GetVisible();
}

void EcheTray::SetEcheConnectionStatusHandler(
    eche_app::EcheConnectionStatusHandler* eche_connection_status_handler) {
  if (features::IsEcheNetworkConnectionStateEnabled()) {
    eche_connection_status_handler_ = eche_connection_status_handler;
    eche_connection_status_handler_->AddObserver(this);
  }
}

bool EcheTray::IsBackgroundConnectionAttemptInProgress() {
  return initializer_webview_ ? true : false;
}

BEGIN_METADATA(EcheTray)
END_METADATA

}  // namespace ash