chromium/ash/system/cast/cast_notification_controller.cc

// Copyright 2018 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/cast/cast_notification_controller.h"

#include "ash/constants/notifier_catalogs.h"
#include "ash/public/cpp/notification_utils.h"
#include "ash/public/cpp/system_notification_builder.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/root_window_controller.h"
#include "ash/shelf/shelf.h"
#include "ash/shelf/shelf_focus_cycler.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/system/notification_center/notification_center_tray.h"
#include "ash/system/status_area_widget.h"
#include "ash/system/unified/unified_system_tray.h"
#include "ash/system/unified/unified_system_tray_bubble.h"
#include "base/functional/bind.h"
#include "base/metrics/user_metrics.h"
#include "base/strings/utf_string_conversions.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/message_center/message_center.h"
#include "ui/message_center/public/cpp/notification.h"
#include "ui/views/widget/widget.h"

using message_center::MessageCenter;
using message_center::Notification;

namespace ash {

namespace {

bool ShouldShowNotification() {
  auto* cast_config = CastConfigController::Get();
  return cast_config && cast_config->HasSinksAndRoutes() &&
         cast_config->HasActiveRoute();
}

std::u16string GetNotificationTitle(const CastSink& sink,
                                    const CastRoute& route) {
  switch (route.content_source) {
    case ContentSource::kUnknown:
      return l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_CAST_CAST_UNKNOWN);
    case ContentSource::kTab:
    case ContentSource::kDesktop:
      return l10n_util::GetStringFUTF16(
          IDS_ASH_STATUS_TRAY_CAST_NOTIFICATION_TITLE,
          base::UTF8ToUTF16(sink.name));
  }
}

const char kNotificationId[] = "chrome://cast";

}  // namespace

CastNotificationController::CastNotificationController() {
  if (CastConfigController::Get()) {
    CastConfigController::Get()->AddObserver(this);
    CastConfigController::Get()->RequestDeviceRefresh();
  }
}

CastNotificationController::~CastNotificationController() {
  if (CastConfigController::Get())
    CastConfigController::Get()->RemoveObserver(this);
}

void CastNotificationController::OnDevicesUpdated(
    const std::vector<SinkAndRoute>& devices) {
  if (!ShouldShowNotification()) {
    message_center::MessageCenter::Get()->RemoveNotification(
        kNotificationId, false /* by_user */);
    return;
  }

  // The cast notification controller outlives cast sessions. Ensure
  // `freeze_button_index_` starts reset when creating a new notification.
  freeze_button_index_.reset();

  for (const auto& device : devices) {
    const CastSink& sink = device.sink;
    const CastRoute& route = device.route;

    // We only want to display casts that came from this machine, since on a
    // busy network many other people could be casting.
    if (route.id.empty() || !route.is_local_source)
      continue;

    displayed_route_id_ = route.id;

    message_center::RichNotificationData data;
    data.pinned = true;

    if (route.freeze_info.can_freeze) {
      displayed_route_is_frozen_ = route.freeze_info.is_frozen;

      // The new pinned notification UI uses icon instead of label buttons.
      if (features::AreOngoingProcessesEnabled()) {
        data.buttons.emplace_back(
            displayed_route_is_frozen_
                ? message_center::ButtonInfo(
                      /*vector_icon=*/&kNotificationPlayIcon,
                      /*accessible_name=*/l10n_util::GetStringUTF16(
                          IDS_ASH_STATUS_TRAY_CAST_RESUME))
                : message_center::ButtonInfo(
                      /*vector_icon=*/&kNotificationPauseIcon,
                      /*accessible_name=*/l10n_util::GetStringUTF16(
                          IDS_ASH_STATUS_TRAY_CAST_PAUSE)));
      } else {
        data.buttons.emplace_back(message_center::ButtonInfo(
            displayed_route_is_frozen_
                ? l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_CAST_RESUME)
                : l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_CAST_PAUSE)));
      }

      freeze_button_index_ = data.buttons.size() - 1;
    }

    // The new pinned notification UI uses icon instead of label buttons.
    if (features::AreOngoingProcessesEnabled()) {
      data.buttons.emplace_back(message_center::ButtonInfo(
          /*vector_icon=*/&kNotificationStopIcon,
          /*accessible_name=*/l10n_util::GetStringUTF16(
              IDS_ASH_STATUS_TRAY_CAST_STOP)));
    } else {
      data.buttons.emplace_back(message_center::ButtonInfo(
          l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_CAST_STOP)));
    }

    std::unique_ptr<Notification> notification =
        ash::SystemNotificationBuilder()
            .SetId(kNotificationId)
            .SetCatalogName(NotificationCatalogName::kCast)
            .SetTitle(GetNotificationTitle(sink, route))
            .SetOptionalFields(data)
            .SetDelegate(base::MakeRefCounted<
                         message_center::HandleNotificationClickDelegate>(
                base::BindRepeating(
                    &CastNotificationController::PressedCallback,
                    weak_ptr_factory_.GetWeakPtr())))
            .SetSmallImage(displayed_route_is_frozen_
                               ? kSystemMenuCastPausedIcon
                               : kSystemMenuCastIcon)
            .BuildPtr(
                /*keep_timestamp=*/false);

    MessageCenter::Get()->AddNotification(std::move(notification));

    break;
  }
}

void CastNotificationController::PressedCallback(
    std::optional<int> button_index) {
  if (freeze_button_index_ && button_index == freeze_button_index_) {
    FreezePressed();
  } else if (button_index) {
    // Handles the case that the stop button is pressed
    StopCasting();
  }
}

void CastNotificationController::StopCasting() {
  CastConfigController::Get()->StopCasting(displayed_route_id_);
  base::RecordAction(base::UserMetricsAction("StatusArea_Cast_StopCast"));
}

void CastNotificationController::FreezePressed() {
  auto* controller = CastConfigController::Get();
  if (displayed_route_is_frozen_) {
    controller->UnfreezeRoute(displayed_route_id_);
  } else {
    auto* status_area_widget =
        Shell::GetPrimaryRootWindowController()->shelf()->GetStatusAreaWidget();
    if (status_area_widget->unified_system_tray() &&
        status_area_widget->unified_system_tray()
            ->IsBubbleShown()) {  // The system tray is open.
      freeze_on_tray_widget_destroyed_ = true;
      status_area_widget->unified_system_tray()->GetBubbleWidget()->AddObserver(
          this);
      status_area_widget->unified_system_tray()->CloseBubble();
      Shell::GetPrimaryRootWindowController()
          ->shelf()
          ->shelf_focus_cycler()
          ->FocusStatusArea(false);
      status_area_widget->unified_system_tray()->RequestFocus();
    } else if (status_area_widget->notification_center_tray() &&
               status_area_widget->notification_center_tray()
                   ->IsBubbleShown()) {  // Notification tray is open.
      freeze_on_tray_widget_destroyed_ = true;
      status_area_widget->notification_center_tray()
          ->GetBubbleWidget()
          ->AddObserver(this);
      status_area_widget->notification_center_tray()->CloseBubble();
      Shell::GetPrimaryRootWindowController()
          ->shelf()
          ->shelf_focus_cycler()
          ->FocusStatusArea(false);
      status_area_widget->notification_center_tray()->RequestFocus();
    } else {
      controller->FreezeRoute(displayed_route_id_);
    }
  }
}

void CastNotificationController::OnWidgetDestroyed(views::Widget* widget) {
  widget->RemoveObserver(this);
  if (freeze_on_tray_widget_destroyed_) {
    CastConfigController::Get()->FreezeRoute(displayed_route_id_);
    freeze_on_tray_widget_destroyed_ = false;
  }
}

}  // namespace ash