chromium/ash/system/notification_center/ash_message_popup_collection.cc

// Copyright 2014 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/notification_center/ash_message_popup_collection.h"

#include <cstddef>
#include <cstdint>
#include <memory>

#include "ash/constants/ash_constants.h"
#include "ash/constants/ash_features.h"
#include "ash/focus_cycler.h"
#include "ash/public/cpp/message_center/arc_notification_constants.h"
#include "ash/public/cpp/shelf_types.h"
#include "ash/public/cpp/shell_window_ids.h"
#include "ash/root_window_controller.h"
#include "ash/shelf/hotseat_widget.h"
#include "ash/shelf/shelf.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/system/notification_center/fullscreen_notification_blocker.h"
#include "ash/system/notification_center/message_center_constants.h"
#include "ash/system/notification_center/message_center_utils.h"
#include "ash/system/notification_center/message_view_factory.h"
#include "ash/system/notification_center/metrics_utils.h"
#include "ash/system/notification_center/notification_style_utils.h"
#include "ash/system/status_area_widget.h"
#include "ash/system/status_area_widget_delegate.h"
#include "ash/system/tray/system_tray_notifier.h"
#include "ash/system/tray/tray_background_view.h"
#include "ash/system/tray/tray_bubble_view.h"
#include "ash/system/tray/tray_utils.h"
#include "ash/system/unified/unified_system_tray.h"
#include "ash/wm/window_properties.h"
#include "ash/wm/work_area_insets.h"
#include "base/auto_reset.h"
#include "base/check.h"
#include "base/i18n/rtl.h"
#include "base/metrics/histogram_functions.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/compositor/compositor.h"
#include "ui/display/display.h"
#include "ui/display/screen.h"
#include "ui/display/tablet_state.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/native_widget_types.h"
#include "ui/message_center/message_center.h"
#include "ui/message_center/public/cpp/message_center_constants.h"
#include "ui/message_center/public/cpp/notification_types.h"
#include "ui/message_center/views/message_popup_collection.h"
#include "ui/message_center/views/message_popup_view.h"
#include "ui/message_center/views/message_view.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/widget/widget.h"
#include "ui/wm/core/shadow_types.h"

namespace ash {

namespace {

const int kPopupMarginX = 8;

void ReportPopupAnimationSmoothness(int smoothness) {
  base::UmaHistogramPercentage("Ash.NotificationPopup.AnimationSmoothness",
                               smoothness);
}

}  // namespace

const char AshMessagePopupCollection::kMessagePopupWidgetName[] =
    "ash/message_center/MessagePopup";

///////////////////////////////////////////////////////////////////////////////
// NotifierCollisionHandler:

AshMessagePopupCollection::NotifierCollisionHandler::NotifierCollisionHandler(
    AshMessagePopupCollection* popup_collection)
    : popup_collection_(popup_collection) {
  Shell::Get()->system_tray_notifier()->AddSystemTrayObserver(this);
  popup_collection_->shelf_->AddObserver(this);
}

AshMessagePopupCollection::NotifierCollisionHandler::
    ~NotifierCollisionHandler() {
  popup_collection_->shelf_->RemoveObserver(this);
  Shell::Get()->system_tray_notifier()->RemoveSystemTrayObserver(this);
}

void AshMessagePopupCollection::NotifierCollisionHandler::
    OnPopupCollectionHeightChanged() {
  if (!features::IsNotifierCollisionEnabled()) {
    return;
  }

  // Ignore changes happen to the popup collection height when bubble changes is
  // being handled. This is to avoid crashes (b/305781721) when we handle both
  // the bubble and the collection height changes at the same time.
  if (is_handling_bubble_change_) {
    return;
  }

  // Do nothing if there's no open corner anchored shelf pod bubble.
  auto* status_area =
      StatusAreaWidget::ForWindow(popup_collection_->shelf_->GetWindow());
  auto* shelf_pod_bubble =
      status_area ? status_area->open_shelf_pod_bubble() : nullptr;
  if (!shelf_pod_bubble || !shelf_pod_bubble->IsAnchoredToShelfCorner()) {
    return;
  }

  // If the popups do not fit in the available space, close the bubble.
  if (popup_collection_->popup_collection_bounds().height() >
      popup_collection_->GetBaseline()) {
    shelf_pod_bubble->CloseBubbleView();
    popup_collection_->MoveDownPopups();

    // Reset bounds so popup baseline is updated.
    popup_collection_->ResetBounds();
  } else {
    // Record metrics if the bubble stays open.
    RecordOnTopOfSurfacesPopupCount();
  }
}

int AshMessagePopupCollection::NotifierCollisionHandler::
    CalculateBaselineOffset() {
  // Baseline pre-notifier collision does not consider corner anchored shelf pod
  // bubbles or slider bubbles to set its offset.
  if (!features::IsNotifierCollisionEnabled()) {
    surface_type_ = NotifierCollisionSurfaceType::kExtendedHotseat;
    return CalculateExtendedHotseatOffset();
  }

  auto* status_area =
      StatusAreaWidget::ForWindow(popup_collection_->shelf_->GetWindow());
  auto* current_open_shelf_pod_bubble =
      status_area ? status_area->open_shelf_pod_bubble() : nullptr;

  if (current_open_shelf_pod_bubble &&
      current_open_shelf_pod_bubble->IsAnchoredToShelfCorner()) {
    // Offset is calculated based on the height of the corner anchored shelf pod
    // bubble, if one is open.
    baseline_offset_ = current_open_shelf_pod_bubble->height() +
                       message_center::kMarginBetweenPopups;
    surface_type_ = NotifierCollisionSurfaceType::kShelfPodBubble;
  } else {
    int slider_offset = CalculateSliderOffset();
    int hotseat_offset = CalculateExtendedHotseatOffset();

    // If no corner anchored shelf pod bubble is open, the offset is calculated
    // based on the visibility of slider bubbles and the extended hotseat.
    baseline_offset_ = slider_offset + hotseat_offset;

    if (slider_offset != 0 && hotseat_offset != 0) {
      surface_type_ =
          NotifierCollisionSurfaceType::kSliderBubbleAndExtendedHotseat;
    } else if (slider_offset != 0) {
      surface_type_ = NotifierCollisionSurfaceType::kSliderBubble;
    } else if (hotseat_offset != 0) {
      surface_type_ = NotifierCollisionSurfaceType::kExtendedHotseat;
    } else {
      surface_type_ = NotifierCollisionSurfaceType::kNone;
    }
  }

  return baseline_offset_;
}

void AshMessagePopupCollection::NotifierCollisionHandler::
    OnStatusAreaAnchoredBubbleVisibilityChanged(TrayBubbleView* tray_bubble,
                                                bool visible) {
  HandleBubbleVisibilityOrBoundsChanged();
}

void AshMessagePopupCollection::NotifierCollisionHandler::
    OnTrayBubbleBoundsChanged(TrayBubbleView* tray_bubble) {
  HandleBubbleVisibilityOrBoundsChanged();
}

void AshMessagePopupCollection::NotifierCollisionHandler::
    HandleBubbleVisibilityOrBoundsChanged() {
  if (!features::IsNotifierCollisionEnabled()) {
    return;
  }

  // This is to make sure that we don't close the bubble through
  // `OnPopupCollectionHeightChanged()` to avoid crashes (b/305781721).
  base::AutoReset<bool> reset(&is_handling_bubble_change_, true);

  int previous_baseline_offset = baseline_offset_;

  // If the popup collection does not fit in the available space when opening a
  // bubble or updating its height, close all popups.
  if (popup_collection_->popup_collection_bounds().height() >
      popup_collection_->GetBaseline()) {
    popup_collection_->CloseAllPopupsNow();
  }

  // Reset bounds so popup baseline is updated.
  popup_collection_->ResetBounds();

  if (baseline_offset_ != previous_baseline_offset && baseline_offset_ != 0) {
    RecordOnTopOfSurfacesPopupCount();
    RecordSurfaceType();
  }
}

int AshMessagePopupCollection::NotifierCollisionHandler::
    CalculateExtendedHotseatOffset() const {
  auto* hotseat_widget = popup_collection_->shelf_->hotseat_widget();

  // `hotseat_widget` might be null since it dtor-ed before this class.
  return (hotseat_widget && hotseat_widget->state() == HotseatState::kExtended)
             ? hotseat_widget->GetHotseatSize()
             : 0;
}

int AshMessagePopupCollection::NotifierCollisionHandler::CalculateSliderOffset()
    const {
  auto* root_window_controller =
      RootWindowController::ForWindow(popup_collection_->shelf_->GetWindow());

  if (!root_window_controller ||
      !root_window_controller->GetStatusAreaWidget()) {
    return 0;
  }

  auto* unified_system_tray =
      root_window_controller->GetStatusAreaWidget()->unified_system_tray();

  return (unified_system_tray && unified_system_tray->IsSliderBubbleShown() &&
          unified_system_tray->GetSliderView())
             ? unified_system_tray->GetSliderView()->height() +
                   message_center::kMarginBetweenPopups
             : 0;
}

void AshMessagePopupCollection::NotifierCollisionHandler::
    RecordOnTopOfSurfacesPopupCount() {
  size_t popup_count = popup_collection_->popup_items().size();
  if (popup_count != 0) {
    base::UmaHistogramCounts100(
        "Ash.NotificationPopup.OnTopOfSurfacesPopupCount", popup_count);
  }
}

void AshMessagePopupCollection::NotifierCollisionHandler::RecordSurfaceType() {
  if (popup_collection_->popup_items().size() != 0) {
    base::UmaHistogramEnumeration("Ash.NotificationPopup.OnTopOfSurfacesType",
                                  surface_type_);
  }
}

void AshMessagePopupCollection::NotifierCollisionHandler::
    OnDisplayTabletStateChanged(display::TabletState state) {
  if (display::IsTabletStateChanging(state)) {
    // Do nothing when the tablet state is still in the process of transition.
    return;
  }

  // Reset bounds so pop-up baseline is updated.
  popup_collection_->ResetBounds();
}

void AshMessagePopupCollection::NotifierCollisionHandler::
    OnBackgroundTypeChanged(ShelfBackgroundType background_type,
                            AnimationChangeType change_type) {
  popup_collection_->ResetBounds();
}

void AshMessagePopupCollection::NotifierCollisionHandler::
    OnShelfWorkAreaInsetsChanged() {
  popup_collection_->UpdateWorkArea();
}

void AshMessagePopupCollection::NotifierCollisionHandler::OnHotseatStateChanged(
    HotseatState old_state,
    HotseatState new_state) {
  // We only need to take care of `HotseatState::kExtended` state.
  if (old_state != HotseatState::kExtended &&
      new_state != HotseatState::kExtended) {
    return;
  }
  popup_collection_->ResetBounds();
  RecordSurfaceType();
}

///////////////////////////////////////////////////////////////////////////////
// AshMessagePopupCollection:

AshMessagePopupCollection::AshMessagePopupCollection(display::Screen* screen,
                                                     Shelf* shelf)
    : screen_(screen), shelf_(shelf) {
  notifier_collision_handler_ =
      std::make_unique<NotifierCollisionHandler>(this);
  StartObserving(screen_,
                 screen_->GetDisplayNearestWindow(
                     shelf_->GetStatusAreaWidget()->GetNativeWindow()));
}

AshMessagePopupCollection::~AshMessagePopupCollection() {
  for (views::Widget* widget : tracked_widgets_)
    widget->RemoveObserver(this);
  CHECK(!views::WidgetObserver::IsInObserverList());

  // Should destruct `notifier_collision_handler_` before all other instances of
  // this class since the handler depends on some of them.
  notifier_collision_handler_.reset();
}

void AshMessagePopupCollection::StartObserving(
    display::Screen* screen,
    const display::Display& display) {
  screen_ = screen;
  work_area_ = display.work_area();
  display_observer_.emplace(this);
}

int AshMessagePopupCollection::GetPopupOriginX(
    const gfx::Rect& popup_bounds) const {
  // Popups should always follow the status area and will usually show on the
  // bottom-right of the screen. They will show at the bottom-left whenever the
  // shelf is left-aligned or for RTL when the shelf is not right aligned.
  return ((base::i18n::IsRTL() && GetAlignment() != ShelfAlignment::kRight) ||
          IsFromLeft())
             ? work_area_.x() + kPopupMarginX
             : work_area_.right() - kPopupMarginX - popup_bounds.width();
}

int AshMessagePopupCollection::GetBaseline() const {
  gfx::Insets tray_bubble_insets = GetTrayBubbleInsets(shelf_->GetWindow());
  int notifier_collision_offset =
      notifier_collision_handler_
          ? notifier_collision_handler_->CalculateBaselineOffset()
          : 0;

  // Decrease baseline by `kShelfDisplayOffset` to compensate for the adjustment
  // of edges in `Shelf::GetSystemTrayAnchorRect()`.
  return work_area_.bottom() - tray_bubble_insets.bottom() -
         notifier_collision_offset - kShelfDisplayOffset;
}

gfx::Rect AshMessagePopupCollection::GetWorkArea() const {
  return work_area_;
}

bool AshMessagePopupCollection::IsTopDown() const {
  return false;
}

bool AshMessagePopupCollection::IsFromLeft() const {
  return GetAlignment() == ShelfAlignment::kLeft;
}

bool AshMessagePopupCollection::RecomputeAlignment(
    const display::Display& display) {
  // Nothing needs to be done.
  return false;
}

void AshMessagePopupCollection::ConfigureWidgetInitParamsForContainer(
    views::Widget* widget,
    views::Widget::InitParams* init_params) {
  init_params->shadow_type = views::Widget::InitParams::ShadowType::kDrop;
  init_params->shadow_elevation = ::wm::kShadowElevationInactiveWindow;
  // On ash, popups go in `SettingBubbleContainer` together with other tray
  // bubbles, so the most recent element on screen will appear in front.
  init_params->parent = shelf_->GetWindow()->GetRootWindow()->GetChildById(
      kShellWindowId_SettingBubbleContainer);

  // Make the widget activatable so it can receive focus when cycling through
  // windows (i.e. pressing ctrl + forward/back).
  init_params->activatable = views::Widget::InitParams::Activatable::kYes;
  init_params->name = kMessagePopupWidgetName;
  init_params->corner_radius = kMessagePopupCornerRadius;
  init_params->init_properties_container.SetProperty(
      kStayInOverviewOnActivationKey, true);
  Shell::Get()->focus_cycler()->AddWidget(widget);
  widget->AddObserver(this);
  tracked_widgets_.insert(widget);
}

bool AshMessagePopupCollection::IsPrimaryDisplayForNotification() const {
  return screen_ &&
         GetCurrentDisplay().id() == screen_->GetPrimaryDisplay().id();
}

bool AshMessagePopupCollection::BlockForMixedFullscreen(
    const message_center::Notification& notification) const {
  return FullscreenNotificationBlocker::BlockForMixedFullscreen(
      notification, RootWindowController::ForWindow(shelf_->GetWindow())
                        ->IsInFullscreenMode());
}

void AshMessagePopupCollection::NotifyPopupAdded(
    message_center::MessagePopupView* popup) {
  MessagePopupCollection::NotifyPopupAdded(popup);
  popup->message_view()->AddObserver(this);
  metrics_utils::LogPopupShown(popup->message_view()->notification_id());
  last_pop_up_added_ = popup;
}

void AshMessagePopupCollection::NotifyPopupClosed(
    message_center::MessagePopupView* popup) {
  metrics_utils::LogPopupClosed(popup);
  MessagePopupCollection::NotifyPopupClosed(popup);
  popup->message_view()->RemoveObserver(this);
  if (last_pop_up_added_ == popup)
    last_pop_up_added_ = nullptr;
}

void AshMessagePopupCollection::NotifySilentNotification(
    const std::string& notification_id) {
  // Have any active screen reader announce the incoming silent notification.
  const views::View* status_area_widget_delegate =
      shelf_->GetStatusAreaWidget()->status_area_widget_delegate();
  CHECK(status_area_widget_delegate);
  status_area_widget_delegate->GetViewAccessibility().AnnounceText(
      l10n_util::GetStringFUTF16Int(
          IDS_ASH_MESSAGE_CENTER_SILENT_NOTIFICATION_ANNOUNCEMENT,
          (int)message_center::MessageCenter::Get()->NotificationCount()));
}

void AshMessagePopupCollection::NotifyPopupCollectionHeightChanged() {
  if (!notifier_collision_handler_) {
    return;
  }

  notifier_collision_handler_->OnPopupCollectionHeightChanged();
}

void AshMessagePopupCollection::AnimationStarted() {
  if (popups_animating_ == 0 && last_pop_up_added_) {
    // Since all the popup widgets use the same compositor, we only need to set
    // this when the first popup shows in the animation sequence.
    animation_tracker_.emplace(last_pop_up_added_->GetWidget()
                                   ->GetCompositor()
                                   ->RequestNewThroughputTracker());
    animation_tracker_->Start(metrics_util::ForSmoothnessV3(
        base::BindRepeating(&ReportPopupAnimationSmoothness)));
  }
  ++popups_animating_;
}

void AshMessagePopupCollection::AnimationFinished() {
  --popups_animating_;
  if (!popups_animating_) {
    // Stop tracking when all animations are finished.
    if (animation_tracker_) {
      animation_tracker_->Stop();
      animation_tracker_.reset();
    }

    if (animation_idle_closure_) {
      std::move(animation_idle_closure_).Run();
    }
  }
}

message_center::MessagePopupView* AshMessagePopupCollection::CreatePopup(
    const message_center::Notification& notification) {
  bool a11_feedback_on_init =
      notification.rich_notification_data()
          .should_make_spoken_feedback_for_popup_updates;
  auto* popup_view = new message_center::MessagePopupView(
      MessageViewFactory::Create(notification, /*shown_in_popup=*/true)
          .release(),
      this, a11_feedback_on_init);

  // Custom arc notifications handle their own styling and background.
  if (notification.custom_view_type() != kArcNotificationCustomViewType) {
    notification_style_utils::StyleNotificationPopup(
        popup_view->message_view());
  }
  return popup_view;
}

void AshMessagePopupCollection::ClosePopupItem(PopupItem& item) {
  // We lock closing tray bubble here to prevent a bubble close when popup item
  // is removed (b/291988617).
  auto lock = TrayBackgroundView::DisableCloseBubbleOnWindowActivated();

  message_center::MessagePopupCollection::ClosePopupItem(item);
}

bool AshMessagePopupCollection::IsWidgetAPopupNotification(
    views::Widget* widget) {
  for (views::Widget* popup_widget : tracked_widgets_) {
    if (widget == popup_widget) {
      return true;
    }
  }
  return false;
}

void AshMessagePopupCollection::SetAnimationIdleClosureForTest(
    base::OnceClosure closure) {
  DCHECK(closure);
  DCHECK(!animation_idle_closure_);
  animation_idle_closure_ = std::move(closure);
}

void AshMessagePopupCollection::OnSlideOut(const std::string& notification_id) {
  metrics_utils::LogClosedByUser(notification_id, /*is_swipe=*/true,
                                 /*is_popup=*/true);
}

void AshMessagePopupCollection::OnCloseButtonPressed(
    const std::string& notification_id) {
  metrics_utils::LogClosedByUser(notification_id, /*is_swipe=*/false,
                                 /*is_popup=*/true);
}

void AshMessagePopupCollection::OnSettingsButtonPressed(
    const std::string& notification_id) {
  metrics_utils::LogSettingsShown(notification_id, /*is_slide_controls=*/false,
                                  /*is_popup=*/true);
}

void AshMessagePopupCollection::OnSnoozeButtonPressed(
    const std::string& notification_id) {
  metrics_utils::LogSnoozed(notification_id, /*is_slide_controls=*/false,
                            /*is_popup=*/true);
}

ShelfAlignment AshMessagePopupCollection::GetAlignment() const {
  return shelf_->alignment();
}

display::Display AshMessagePopupCollection::GetCurrentDisplay() const {
  return display::Screen::GetScreen()->GetDisplayNearestWindow(
      shelf_->GetWindow());
}

void AshMessagePopupCollection::UpdateWorkArea() {
  gfx::Rect new_work_area =
      WorkAreaInsets::ForWindow(shelf_->GetWindow()->GetRootWindow())
          ->user_work_area_bounds();
  if (work_area_ == new_work_area)
    return;

  work_area_ = new_work_area;
  ResetBounds();
}

void AshMessagePopupCollection::OnDisplayMetricsChanged(
    const display::Display& display,
    uint32_t metrics) {
  if (GetCurrentDisplay().id() == display.id())
    UpdateWorkArea();
}

///////////////////////////////////////////////////////////////////////////////
// views::WidgetObserver:

void AshMessagePopupCollection::OnWidgetClosing(views::Widget* widget) {
  Shell::Get()->focus_cycler()->RemoveWidget(widget);
  widget->RemoveObserver(this);
  tracked_widgets_.erase(widget);
}

void AshMessagePopupCollection::OnWidgetActivationChanged(views::Widget* widget,
                                                          bool active) {
  // Note: Each pop-up is contained in it's own widget and we need to manually
  // focus the contained MessageView when the widget is activated through the
  // FocusCycler.
  if (active && Shell::Get()->focus_cycler()->widget_activating() == widget)
    widget->GetFocusManager()->SetFocusedView(widget->GetContentsView());
}

}  // namespace ash