// Copyright 2012 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/media/webrtc/media_stream_capture_indicator.h"

#include <stddef.h>

#include <memory>
#include <string>
#include <utility>

#include "base/check_op.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/notreached.h"
#include "base/observer_list.h"
#include "build/build_config.h"
#include "chrome/app/chrome_command_ids.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/content_settings/chrome_content_settings_utils.h"
#include "chrome/browser/media/webrtc/capture_policy_utils.h"
#include "chrome/browser/media/webrtc/same_origin_observer.h"
#include "chrome/browser/status_icons/status_icon.h"
#include "chrome/browser/status_icons/status_tray.h"
#include "chrome/browser/tab_contents/tab_util.h"
#include "components/url_formatter/elide_url.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_delegate.h"
#include "content/public/browser/web_contents_observer.h"
#include "extensions/buildflags/buildflags.h"
#include "third_party/blink/public/mojom/mediastream/media_stream.mojom.h"
#include "ui/base/ui_base_features.h"
#include "ui/gfx/image/image_skia.h"

#if !BUILDFLAG(IS_ANDROID)
#include "chrome/browser/media/webrtc/media_stream_focus_delegate.h"
#include "chrome/grit/branded_strings.h"
#include "components/vector_icons/vector_icons.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/gfx/color_palette.h"
#include "ui/gfx/paint_vector_icon.h"
#endif

#if BUILDFLAG(ENABLE_EXTENSIONS)
#include "base/strings/utf_string_conversions.h"
#include "chrome/common/extensions/extension_constants.h"
#include "extensions/browser/extension_registry.h"  // nogncheck
#include "extensions/common/extension.h"
#endif

#if BUILDFLAG(IS_CHROMEOS)
#include "chrome/browser/chromeos/policy/dlp/dlp_content_manager.h"
#endif

using content::BrowserThread;
using content::WebContents;

namespace {

constexpr int kMaxMediaStreamCaptureListItems = 100;
constexpr int IDC_MEDIA_CONTEXT_MEDIA_STREAM_CAPTURE_LIST_LAST =
    IDC_MEDIA_CONTEXT_MEDIA_STREAM_CAPTURE_LIST_FIRST +
    kMaxMediaStreamCaptureListItems - 1;

#if BUILDFLAG(ENABLE_EXTENSIONS)
const extensions::Extension* GetExtension(WebContents* web_contents) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  if (!web_contents)
    return nullptr;

  extensions::ExtensionRegistry* registry =
      extensions::ExtensionRegistry::Get(web_contents->GetBrowserContext());
  return registry->enabled_extensions().GetExtensionOrAppByURL(
      web_contents->GetLastCommittedURL());
}

#endif  // BUILDFLAG(ENABLE_EXTENSIONS)

std::unique_ptr<SameOriginObserver> MaybeCreateSameOriginObserverForTabCapture(
    content::WebContents* capturer_web_contents,
    const std::optional<content::DesktopMediaID>& media_id,
    base::RepeatingCallback<void(content::WebContents*)> stop_callback) {
  if (!media_id ||
      media_id->type != content::DesktopMediaID::Type::TYPE_WEB_CONTENTS) {
    return nullptr;
  }

  if (!capture_policy::CapturerRestrictedToSameOrigin(capturer_web_contents)) {
    return nullptr;
  }

  auto* captured_contents = content::WebContents::FromRenderFrameHost(
      content::RenderFrameHost::FromID(
          media_id->web_contents_id.render_process_id,
          media_id->web_contents_id.main_render_frame_id));
  if (!captured_contents) {
    return nullptr;
  }

  const url::Origin capturer_origin =
      capturer_web_contents->GetPrimaryMainFrame()->GetLastCommittedOrigin();
  return std::make_unique<SameOriginObserver>(
      captured_contents, capturer_origin, std::move(stop_callback));
}

std::u16string GetTitle(WebContents* web_contents) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  if (!web_contents)
    return std::u16string();

#if BUILDFLAG(ENABLE_EXTENSIONS)
  const extensions::Extension* const extension = GetExtension(web_contents);
  if (extension)
    return base::UTF8ToUTF16(extension->name());
#endif

  return url_formatter::FormatUrlForSecurityDisplay(
      web_contents->GetLastCommittedURL());
}

bool IsDeviceCapturingTab(const blink::MediaStreamDevice& device) {
  return device.display_media_info &&
         device.display_media_info->display_surface ==
             media::mojom::DisplayCaptureSurfaceType::BROWSER;
}

bool IsDeviceCapturingWindow(const blink::MediaStreamDevice& device) {
  return device.display_media_info &&
         device.display_media_info->display_surface ==
             media::mojom::DisplayCaptureSurfaceType::WINDOW;
}

// Returns if the passed |device| is capturing the whole display. This is
// different from capturing a tab or a single window on a desktop.
bool IsDeviceCapturingDisplay(const blink::MediaStreamDevice& device) {
  return device.display_media_info &&
         device.display_media_info->display_surface ==
             media::mojom::DisplayCaptureSurfaceType::MONITOR;
}

typedef void (MediaStreamCaptureIndicator::Observer::*ObserverMethod)(
    content::WebContents* web_contents,
    bool value);

ObserverMethod GetObserverMethodToCall(const blink::MediaStreamDevice& device) {
  switch (device.type) {
    case blink::mojom::MediaStreamType::DEVICE_AUDIO_CAPTURE:
      return &MediaStreamCaptureIndicator::Observer::OnIsCapturingAudioChanged;

    case blink::mojom::MediaStreamType::DEVICE_VIDEO_CAPTURE:
      return &MediaStreamCaptureIndicator::Observer::OnIsCapturingVideoChanged;

    case blink::mojom::MediaStreamType::GUM_TAB_AUDIO_CAPTURE:
    case blink::mojom::MediaStreamType::GUM_TAB_VIDEO_CAPTURE:
      return &MediaStreamCaptureIndicator::Observer::OnIsBeingMirroredChanged;

    case blink::mojom::MediaStreamType::GUM_DESKTOP_VIDEO_CAPTURE:
    case blink::mojom::MediaStreamType::GUM_DESKTOP_AUDIO_CAPTURE:
    case blink::mojom::MediaStreamType::DISPLAY_VIDEO_CAPTURE:
    case blink::mojom::MediaStreamType::DISPLAY_AUDIO_CAPTURE:
    case blink::mojom::MediaStreamType::DISPLAY_VIDEO_CAPTURE_THIS_TAB:
    case blink::mojom::MediaStreamType::DISPLAY_VIDEO_CAPTURE_SET:
      if (IsDeviceCapturingTab(device)) {
        return &MediaStreamCaptureIndicator::Observer::OnIsCapturingTabChanged;
      }
      if (IsDeviceCapturingWindow(device)) {
        return &MediaStreamCaptureIndicator::Observer::
            OnIsCapturingWindowChanged;
      }
      if (IsDeviceCapturingDisplay(device)) {
        return &MediaStreamCaptureIndicator::Observer::
            OnIsCapturingDisplayChanged;
      }
      NOTREACHED();

    case blink::mojom::MediaStreamType::NO_SERVICE:
    case blink::mojom::MediaStreamType::NUM_MEDIA_TYPES:
      NOTREACHED();
  }
}

}  // namespace

// Stores usage counts for all the capture devices associated with a single
// WebContents instance. Instances of this class are owned by
// MediaStreamCaptureIndicator. They also observe for the destruction of their
// corresponding WebContents and trigger their own deletion from their
// MediaStreamCaptureIndicator.
class MediaStreamCaptureIndicator::WebContentsDeviceUsage
    : public content::WebContentsObserver {
 public:
  WebContentsDeviceUsage(scoped_refptr<MediaStreamCaptureIndicator> indicator,
                         WebContents* web_contents)
      : WebContentsObserver(web_contents), indicator_(std::move(indicator)) {}

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

  ~WebContentsDeviceUsage() override = default;

  bool IsCapturingAudio() const { return audio_stream_count_ > 0; }
  bool IsCapturingVideo() const { return video_stream_count_ > 0; }
  bool IsMirroring() const { return mirroring_stream_count_ > 0; }
  bool IsCapturingTab() const { return tab_stream_count_ > 0; }
  bool IsCapturingWindow() const { return window_stream_count_ > 0; }
  bool IsCapturingDisplay() const { return display_stream_count_ > 0; }

  std::unique_ptr<content::MediaStreamUI> RegisterMediaStream(
      const blink::mojom::StreamDevices& devices,
      std::unique_ptr<MediaStreamUI> ui,
      const std::u16string application_title,
      const std::optional<content::DesktopMediaID>& media_id);

  // Increment ref-counts up based on the type of each device provided.
  void AddDevices(const blink::mojom::StreamDevices& devices,
                  base::OnceClosure stop_callback,
                  int stop_callback_id);

  // Decrement ref-counts up based on the type of each device provided.
  void RemoveDevices(const blink::mojom::StreamDevices& devices,
                     int stop_callback_id);

  void StopMediaCapturing(int media_type);

 private:
  int& GetStreamCount(const blink::MediaStreamDevice& device);

  void AddDevice(const blink::MediaStreamDevice& device);

  void RemoveDevice(const blink::MediaStreamDevice& device);

  // content::WebContentsObserver overrides.
  void WebContentsDestroyed() override {
    indicator_->UnregisterWebContents(web_contents());
  }

  void StopCallbacks(std::map<int, base::OnceClosure>& stop_callbacks);

  scoped_refptr<MediaStreamCaptureIndicator> indicator_;
  int audio_stream_count_ = 0;
  int video_stream_count_ = 0;
  int mirroring_stream_count_ = 0;
  int tab_stream_count_ = 0;
  int window_stream_count_ = 0;
  int display_stream_count_ = 0;

  std::map<int, base::OnceClosure> display_media_stop_callbacks_;
  std::map<int, base::OnceClosure> user_media_stop_callbacks_;
  base::WeakPtrFactory<WebContentsDeviceUsage> weak_factory_{this};
};

// Implements MediaStreamUI interface. Instances of this class are created for
// each MediaStream and their ownership is passed to MediaStream implementation
// in the content layer. Each UIDelegate keeps a weak pointer to the
// corresponding WebContentsDeviceUsage object to deliver updates about state of
// the stream.
class MediaStreamCaptureIndicator::UIDelegate : public content::MediaStreamUI {
 public:
  UIDelegate(WebContents* web_contents,
             base::WeakPtr<WebContentsDeviceUsage> device_usage,
             const blink::mojom::StreamDevices& devices,
             std::unique_ptr<::MediaStreamUI> ui,
             const std::u16string application_title,
             std::optional<content::DesktopMediaID> media_id)
      : device_usage_(device_usage),
        devices_(devices),
        ui_(std::move(ui)),
#if !BUILDFLAG(IS_ANDROID)
        focus_delegate_(web_contents),
#endif
        application_title_(std::move(application_title)),
        stop_callback_id_(MediaStreamCaptureIndicator::g_stop_callback_id_++) {
    DCHECK_CURRENTLY_ON(BrowserThread::UI);
    DCHECK(devices_.audio_device.has_value() ||
           devices_.video_device.has_value());

    // base::Unretained is safe here because `same_origin_observer_` is owned by
    // `this`, so the callback will not be invoked after `this` is destroyed.
    same_origin_observer_ = MaybeCreateSameOriginObserverForTabCapture(
        web_contents, media_id,
        base::BindRepeating(&UIDelegate::StopCaptureDueToPolicy,
                            base::Unretained(this)));
  }

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

  ~UIDelegate() override {
    if (started_ && device_usage_)
      device_usage_->RemoveDevices(devices_, stop_callback_id_);
  }

 private:
  // content::MediaStreamUI interface.
  gfx::NativeViewId OnStarted(
      base::RepeatingClosure stop_callback,
      content::MediaStreamUI::SourceCallback source_callback,
      const std::string& label,
      std::vector<content::DesktopMediaID> screen_capture_ids,
      StateChangeCallback state_change_callback) override {
    if (started_) {
      // Ignore possibly-compromised renderers that might call
      // MediaStreamDispatcherHost::OnStreamStarted() more than once.
      // See: https://crbug.com/40054066
      return 0;
    }
    started_ = true;

    if (device_usage_) {
      // |device_usage_| handles |stop_callback| when |ui_| is unspecified.
      device_usage_->AddDevices(devices_, stop_callback, stop_callback_id_);
    }

#if BUILDFLAG(IS_CHROMEOS)
    policy::DlpContentManager::Get()->OnScreenShareStarted(
        label, screen_capture_ids, application_title_, stop_callback,
        state_change_callback, source_callback);
#endif

    // If a custom |ui_| is specified, notify it that the stream started and let
    // it handle the |stop_callback| and |source_callback|.
    if (ui_)
      return ui_->OnStarted(stop_callback, std::move(source_callback),
                            screen_capture_ids);

    return 0;
  }

  void OnDeviceStoppedForSourceChange(
      const std::string& label,
      const content::DesktopMediaID& old_media_id,
      const content::DesktopMediaID& new_media_id,
      bool captured_surface_control_active) override {
#if BUILDFLAG(IS_CHROMEOS)
    policy::DlpContentManager::Get()->OnScreenShareSourceChanging(
        label, old_media_id, new_media_id, captured_surface_control_active);
#endif
  }

  void OnDeviceStopped(const std::string& label,
                       const content::DesktopMediaID& media_id) override {
#if BUILDFLAG(IS_CHROMEOS)
    policy::DlpContentManager::Get()->OnScreenShareStopped(label, media_id);
#endif
  }

  void OnRegionCaptureRectChanged(
      const std::optional<gfx::Rect>& region_capture_rect) override {
    DCHECK_CURRENTLY_ON(BrowserThread::UI);
    if (ui_) {
      ui_->OnRegionCaptureRectChanged(region_capture_rect);
    }
  }

#if !BUILDFLAG(IS_ANDROID)
  void SetFocus(const content::DesktopMediaID& media_id,
                bool focus,
                bool is_from_microtask,
                bool is_from_timer) override {
    focus_delegate_.SetFocus(media_id, focus, is_from_microtask, is_from_timer);
  }
#endif

  void StopCaptureDueToPolicy(content::WebContents* contents) {
    if (device_usage_) {
      device_usage_->StopMediaCapturing(MediaType::kUserMedia |
                                        MediaType::kDisplayMedia);
    }
    capture_policy::ShowCaptureTerminatedDialog(contents);
  }

  base::WeakPtr<WebContentsDeviceUsage> device_usage_;
  const blink::mojom::StreamDevices devices_;
  const std::unique_ptr<::MediaStreamUI> ui_;
#if !BUILDFLAG(IS_ANDROID)
  MediaStreamFocusDelegate focus_delegate_;
#endif
  const std::u16string application_title_;
  bool started_ = false;
  const int stop_callback_id_;
  std::unique_ptr<SameOriginObserver> same_origin_observer_;
};

std::unique_ptr<content::MediaStreamUI>
MediaStreamCaptureIndicator::WebContentsDeviceUsage::RegisterMediaStream(
    const blink::mojom::StreamDevices& devices,
    std::unique_ptr<MediaStreamUI> ui,
    const std::u16string application_title,
    const std::optional<content::DesktopMediaID>& media_id) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  return std::make_unique<UIDelegate>(
      web_contents(), weak_factory_.GetWeakPtr(), devices, std::move(ui),
      std::move(application_title), media_id);
}

void MediaStreamCaptureIndicator::WebContentsDeviceUsage::AddDevices(
    const blink::mojom::StreamDevices& devices,
    base::OnceClosure stop_callback,
    int stop_callback_id) {
  MediaType type = MediaType::kUnknown;

  if (devices.audio_device.has_value()) {
    AddDevice(devices.audio_device.value());
    type = MediaStreamCaptureIndicator::GetMediaType(
        devices.audio_device.value().type);
  }

  if (devices.video_device.has_value()) {
    AddDevice(devices.video_device.value());
    type = MediaStreamCaptureIndicator::GetMediaType(
        devices.video_device.value().type);
  }

  if (type == MediaType::kUserMedia) {
    user_media_stop_callbacks_[stop_callback_id] = std::move(stop_callback);
  }

  if (type == MediaType::kDisplayMedia) {
    display_media_stop_callbacks_[stop_callback_id] = std::move(stop_callback);
  }

  if (web_contents()) {
    web_contents()->NotifyNavigationStateChanged(content::INVALIDATE_TYPE_TAB);
  }

  indicator_->UpdateNotificationUserInterface();
}

void MediaStreamCaptureIndicator::WebContentsDeviceUsage::RemoveDevices(
    const blink::mojom::StreamDevices& devices,
    int stop_callback_id) {
  MediaType type = MediaType::kUnknown;

  if (devices.audio_device.has_value()) {
    RemoveDevice(devices.audio_device.value());
    type = MediaStreamCaptureIndicator::GetMediaType(
        devices.audio_device.value().type);
  }

  if (devices.video_device.has_value()) {
    RemoveDevice(devices.video_device.value());
    type = MediaStreamCaptureIndicator::GetMediaType(
        devices.video_device.value().type);
  }
  if (type == MediaType::kUserMedia) {
    user_media_stop_callbacks_.erase(stop_callback_id);
  }

  if (type == MediaType::kDisplayMedia) {
    display_media_stop_callbacks_.erase(stop_callback_id);
  }

  if (web_contents() && !web_contents()->IsBeingDestroyed()) {
    web_contents()->NotifyNavigationStateChanged(content::INVALIDATE_TYPE_TAB);
    content_settings::UpdateLocationBarUiForWebContents(web_contents());
  }

  indicator_->UpdateNotificationUserInterface();
}

void MediaStreamCaptureIndicator::WebContentsDeviceUsage::StopMediaCapturing(
    int media_type) {
  if (media_type & MediaType::kUserMedia) {
    StopCallbacks(user_media_stop_callbacks_);
  }
  if (media_type & MediaType::kDisplayMedia) {
    StopCallbacks(display_media_stop_callbacks_);
  }
}

void MediaStreamCaptureIndicator::WebContentsDeviceUsage::StopCallbacks(
    std::map<int, base::OnceClosure>& stop_callbacks) {
  // This for loop is implemented in a weird way because std::move on the value
  // will invalid the iterator; we need to record the next iter before
  // std::move.
  for (auto it = stop_callbacks.begin(), it_next = it;
       it != stop_callbacks.end(); it = it_next) {
    ++it_next;
    std::move(it->second).Run();
  }
  stop_callbacks.clear();
}

int& MediaStreamCaptureIndicator::WebContentsDeviceUsage::GetStreamCount(
    const blink::MediaStreamDevice& device) {
  switch (device.type) {
    case blink::mojom::MediaStreamType::DEVICE_AUDIO_CAPTURE:
      return audio_stream_count_;

    case blink::mojom::MediaStreamType::DEVICE_VIDEO_CAPTURE:
      return video_stream_count_;

    case blink::mojom::MediaStreamType::GUM_TAB_AUDIO_CAPTURE:
    case blink::mojom::MediaStreamType::GUM_TAB_VIDEO_CAPTURE:
      return mirroring_stream_count_;

    case blink::mojom::MediaStreamType::GUM_DESKTOP_VIDEO_CAPTURE:
    case blink::mojom::MediaStreamType::GUM_DESKTOP_AUDIO_CAPTURE:
    case blink::mojom::MediaStreamType::DISPLAY_VIDEO_CAPTURE:
    case blink::mojom::MediaStreamType::DISPLAY_AUDIO_CAPTURE:
    case blink::mojom::MediaStreamType::DISPLAY_VIDEO_CAPTURE_THIS_TAB:
    case blink::mojom::MediaStreamType::DISPLAY_VIDEO_CAPTURE_SET:
      if (IsDeviceCapturingTab(device)) {
        return tab_stream_count_;
      }
      if (IsDeviceCapturingWindow(device)) {
        return window_stream_count_;
      }
      if (IsDeviceCapturingDisplay(device)) {
        return display_stream_count_;
      }
      NOTREACHED();

    case blink::mojom::MediaStreamType::NO_SERVICE:
    case blink::mojom::MediaStreamType::NUM_MEDIA_TYPES:
      NOTREACHED();
  }
}

void MediaStreamCaptureIndicator::WebContentsDeviceUsage::AddDevice(
    const blink::MediaStreamDevice& device) {
  int& stream_count = GetStreamCount(device);
  ++stream_count;

  if (web_contents() && stream_count == 1) {
    ObserverMethod obs_func = GetObserverMethodToCall(device);
    DCHECK(obs_func);
    for (Observer& obs : indicator_->observers_)
      (obs.*obs_func)(web_contents(), true);
  }
}

void MediaStreamCaptureIndicator::WebContentsDeviceUsage::RemoveDevice(
    const blink::MediaStreamDevice& device) {
  int& stream_count = GetStreamCount(device);
  --stream_count;
  DCHECK_GE(stream_count, 0);

  if (web_contents() && stream_count == 0) {
    ObserverMethod obs_func = GetObserverMethodToCall(device);
    DCHECK(obs_func);
    for (Observer& obs : indicator_->observers_)
      (obs.*obs_func)(web_contents(), false);
  }
}

// Return whether current device os a screen sharing.
MediaStreamCaptureIndicator::MediaType
MediaStreamCaptureIndicator::GetMediaType(blink::mojom::MediaStreamType type) {
  switch (type) {
    case blink::mojom::MediaStreamType::DEVICE_AUDIO_CAPTURE:
    case blink::mojom::MediaStreamType::DEVICE_VIDEO_CAPTURE:
      return MediaStreamCaptureIndicator::MediaType::kUserMedia;

    case blink::mojom::MediaStreamType::GUM_TAB_AUDIO_CAPTURE:
    case blink::mojom::MediaStreamType::GUM_TAB_VIDEO_CAPTURE:
    case blink::mojom::MediaStreamType::GUM_DESKTOP_VIDEO_CAPTURE:
    case blink::mojom::MediaStreamType::GUM_DESKTOP_AUDIO_CAPTURE:
    case blink::mojom::MediaStreamType::DISPLAY_VIDEO_CAPTURE:
    case blink::mojom::MediaStreamType::DISPLAY_AUDIO_CAPTURE:
    case blink::mojom::MediaStreamType::DISPLAY_VIDEO_CAPTURE_THIS_TAB:
      return MediaStreamCaptureIndicator::MediaType::kDisplayMedia;

    case blink::mojom::MediaStreamType::DISPLAY_VIDEO_CAPTURE_SET:
      return MediaStreamCaptureIndicator::MediaType::kAllScreensMedia;

    default:
      return MediaStreamCaptureIndicator::MediaType::kUnknown;
  }
}

MediaStreamCaptureIndicator::Observer::~Observer() {
  DCHECK(!IsInObserverList());
}

int MediaStreamCaptureIndicator::g_stop_callback_id_ = 0;

MediaStreamCaptureIndicator::MediaStreamCaptureIndicator() = default;

MediaStreamCaptureIndicator::~MediaStreamCaptureIndicator() {
  // The user is responsible for cleaning up by reporting the closure of any
  // opened devices.  However, there exists a race condition at shutdown: The UI
  // thread may be stopped before CaptureDevicesClosed() posts the task to
  // invoke DoDevicesClosedOnUIThread().  In this case, usage_map_ won't be
  // empty like it should.
  DCHECK(usage_map_.empty() ||
         !BrowserThread::IsThreadInitialized(BrowserThread::UI));
}

std::unique_ptr<content::MediaStreamUI>
MediaStreamCaptureIndicator::RegisterMediaStream(
    content::WebContents* web_contents,
    const blink::mojom::StreamDevices& devices,
    std::unique_ptr<MediaStreamUI> ui,
    const std::u16string application_title,
    std::optional<content::DesktopMediaID> media_id) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  DCHECK(web_contents);

  auto& usage = usage_map_[web_contents];
  if (!usage)
    usage = std::make_unique<WebContentsDeviceUsage>(this, web_contents);

  return usage->RegisterMediaStream(devices, std::move(ui),
                                    std::move(application_title), media_id);
}

void MediaStreamCaptureIndicator::ExecuteCommand(int command_id,
                                                 int event_flags) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  const int index =
      command_id - IDC_MEDIA_CONTEXT_MEDIA_STREAM_CAPTURE_LIST_FIRST;
  DCHECK_LE(0, index);
  DCHECK_GT(static_cast<int>(command_targets_.size()), index);
  WebContents* web_contents = command_targets_[index];
  if (usage_map_.contains(web_contents)) {
    web_contents->GetDelegate()->ActivateContents(web_contents);
  }
}

bool MediaStreamCaptureIndicator::CheckUsage(
    content::WebContents* web_contents,
    const WebContentsDeviceUsagePredicate& pred) const {
  auto it = usage_map_.find(web_contents);
  if (it != usage_map_.end() && pred(it->second.get())) {
    return true;
  }

  for (auto* inner_contents : web_contents->GetInnerWebContents()) {
    if (CheckUsage(inner_contents, pred))
      return true;
  }

  return false;
}

bool MediaStreamCaptureIndicator::IsCapturingUserMedia(
    content::WebContents* web_contents) const {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  return CheckUsage(web_contents, [](const WebContentsDeviceUsage* usage) {
    return usage->IsCapturingAudio() || usage->IsCapturingVideo();
  });
}

bool MediaStreamCaptureIndicator::IsCapturingVideo(
    content::WebContents* web_contents) const {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  return CheckUsage(web_contents, &WebContentsDeviceUsage::IsCapturingVideo);
}

bool MediaStreamCaptureIndicator::IsCapturingAudio(
    content::WebContents* web_contents) const {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  return CheckUsage(web_contents, &WebContentsDeviceUsage::IsCapturingAudio);
}

bool MediaStreamCaptureIndicator::IsBeingMirrored(
    content::WebContents* web_contents) const {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  return CheckUsage(web_contents, &WebContentsDeviceUsage::IsMirroring);
}

bool MediaStreamCaptureIndicator::IsCapturingTab(
    content::WebContents* web_contents) const {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  return CheckUsage(web_contents, &WebContentsDeviceUsage::IsCapturingTab);
}

bool MediaStreamCaptureIndicator::IsCapturingWindow(
    content::WebContents* web_contents) const {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  return CheckUsage(web_contents, &WebContentsDeviceUsage::IsCapturingWindow);
}

bool MediaStreamCaptureIndicator::IsCapturingDisplay(
    content::WebContents* web_contents) const {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  return CheckUsage(web_contents, &WebContentsDeviceUsage::IsCapturingDisplay);
}

void MediaStreamCaptureIndicator::StopMediaCapturing(
    content::WebContents* web_contents,
    int media_type) const {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  // AllScreensMedia is an managed device feature, and should not be stopped
  // unless fully discussed.
  CHECK(!(media_type & MediaType::kAllScreensMedia))
      << "AllScreensMedia should not be stopped by MediaStreamCaptureIndicator";

  auto it = usage_map_.find(web_contents);
  if (it != usage_map_.end()) {
    it->second->StopMediaCapturing(media_type);
  }
  for (auto* inner_contents : web_contents->GetInnerWebContents())
    StopMediaCapturing(inner_contents, media_type);
}

void MediaStreamCaptureIndicator::UnregisterWebContents(
    WebContents* web_contents) {
  if (IsCapturingVideo(web_contents)) {
    for (Observer& observer : observers_)
      observer.OnIsCapturingVideoChanged(web_contents, false);
  }
  if (IsCapturingAudio(web_contents)) {
    for (Observer& observer : observers_)
      observer.OnIsCapturingAudioChanged(web_contents, false);
  }
  if (IsBeingMirrored(web_contents)) {
    for (Observer& observer : observers_)
      observer.OnIsBeingMirroredChanged(web_contents, false);
  }
  if (IsCapturingTab(web_contents)) {
    for (Observer& observer : observers_) {
      observer.OnIsCapturingTabChanged(web_contents, false);
    }
  }
  if (IsCapturingWindow(web_contents)) {
    for (Observer& observer : observers_)
      observer.OnIsCapturingWindowChanged(web_contents, false);
  }
  if (IsCapturingDisplay(web_contents)) {
    for (Observer& observer : observers_)
      observer.OnIsCapturingDisplayChanged(web_contents, false);
  }
  usage_map_.erase(web_contents);
  UpdateNotificationUserInterface();
}

void MediaStreamCaptureIndicator::MaybeCreateStatusTrayIcon(bool audio,
                                                            bool video) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  if (status_icon_)
    return;

  // If there is no browser process, we should not create the status tray.
  if (!g_browser_process)
    return;

  StatusTray* status_tray = g_browser_process->status_tray();
  if (!status_tray)
    return;

  gfx::ImageSkia image;
  std::u16string tool_tip;
  GetStatusTrayIconInfo(audio, video, &image, &tool_tip);
  DCHECK(!image.isNull());
  DCHECK(!tool_tip.empty());

  status_icon_ = status_tray->CreateStatusIcon(
      StatusTray::MEDIA_STREAM_CAPTURE_ICON, image, tool_tip);
}

void MediaStreamCaptureIndicator::MaybeDestroyStatusTrayIcon() {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  if (!status_icon_)
    return;

  // If there is no browser process, we should not do anything.
  if (!g_browser_process)
    return;

  StatusTray* status_tray = g_browser_process->status_tray();
  if (status_tray != nullptr) {
    std::unique_ptr<StatusIcon> removed_icon =
        status_tray->RemoveStatusIcon(status_icon_);
    status_icon_ = nullptr;
    removed_icon.reset();
  }
}

void MediaStreamCaptureIndicator::UpdateNotificationUserInterface() {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  std::unique_ptr<StatusIconMenuModel> menu(new StatusIconMenuModel(this));
  bool audio = false;
  bool video = false;
  int command_id = IDC_MEDIA_CONTEXT_MEDIA_STREAM_CAPTURE_LIST_FIRST;
  command_targets_.clear();

  for (const auto& it : usage_map_) {
    // Check if any audio and video devices have been used.
    const WebContentsDeviceUsage& usage = *it.second;
    if (!usage.IsCapturingAudio() && !usage.IsCapturingVideo())
      continue;

    WebContents* const web_contents = it.first;

    // The audio/video icon is shown for extensions or on Android. For
    // regular tabs on desktop, we show an indicator in the tab icon.
#if BUILDFLAG(ENABLE_EXTENSIONS)
    const extensions::Extension* extension = GetExtension(web_contents);
    if (!extension)
      continue;
#endif

    audio = audio || usage.IsCapturingAudio();
    video = video || usage.IsCapturingVideo();

    command_targets_.push_back(web_contents);
    menu->AddItem(command_id, GetTitle(web_contents));

    // If the menu item is not a label, enable it.
    menu->SetCommandIdEnabled(command_id, command_id != IDC_MinimumLabelValue);

    // If reaching the maximum number, no more item will be added to the menu.
    if (command_id == IDC_MEDIA_CONTEXT_MEDIA_STREAM_CAPTURE_LIST_LAST)
      break;
    ++command_id;
  }

  if (command_targets_.empty()) {
    MaybeDestroyStatusTrayIcon();
    return;
  }

  // The icon will take the ownership of the passed context menu.
  MaybeCreateStatusTrayIcon(audio, video);
  if (status_icon_) {
    status_icon_->SetContextMenu(std::move(menu));
  }
}

void MediaStreamCaptureIndicator::GetStatusTrayIconInfo(
    bool audio,
    bool video,
    gfx::ImageSkia* image,
    std::u16string* tool_tip) {
#if BUILDFLAG(IS_ANDROID)
  NOTREACHED();
#else   // !BUILDFLAG(IS_ANDROID)
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  DCHECK(audio || video);
  DCHECK(image);
  DCHECK(tool_tip);

  int message_id = 0;
  const gfx::VectorIcon* icon = nullptr;
  if (audio && video) {
    message_id = IDS_MEDIA_STREAM_STATUS_TRAY_TEXT_AUDIO_AND_VIDEO;
    icon =
        &(features::IsRoundedIconsEnabled() ? vector_icons::kVideocamFilledIcon
                                            : vector_icons::kVideocamOldIcon);
  } else if (audio && !video) {
    message_id = IDS_MEDIA_STREAM_STATUS_TRAY_TEXT_AUDIO_ONLY;
    icon = &(features::IsRoundedIconsEnabled() ? vector_icons::kMicFilledIcon
                                               : vector_icons::kMicOldIcon);
  } else if (!audio && video) {
    message_id = IDS_MEDIA_STREAM_STATUS_TRAY_TEXT_VIDEO_ONLY;
    icon =
        &(features::IsRoundedIconsEnabled() ? vector_icons::kVideocamFilledIcon
                                            : vector_icons::kVideocamOldIcon);
  }

  *tool_tip = l10n_util::GetStringUTF16(message_id);
  *image = gfx::CreateVectorIcon(*icon, 16, gfx::kGoogleGrey700);
#endif  // !BUILDFLAG(IS_ANDROID)
}
