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

#include "content/browser/renderer_host/media/audio_output_authorization_handler.h"

#include "base/functional/bind.h"
#include "base/metrics/histogram_macros.h"
#include "base/task/bind_post_task.h"
#include "base/trace_event/trace_event.h"
#include "content/browser/media/media_devices_permission_checker.h"
#include "content/browser/media/media_devices_util.h"
#include "content/browser/renderer_host/media/audio_input_device_manager.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/media_device_id.h"
#include "content/public/browser/render_frame_host.h"
#include "media/audio/audio_system.h"
#include "third_party/perfetto/include/perfetto/tracing/track.h"

using blink::mojom::MediaDeviceType;

namespace content {

namespace {

void GotSaltAndOrigin(
    GlobalRenderFrameHostId render_frame_host_id,
    bool override_permissions,
    bool permissions_override_value,
    base::OnceCallback<void(MediaDeviceSaltAndOrigin, bool)> cb,
    const MediaDeviceSaltAndOrigin& salt_and_origin) {
  CHECK_CURRENTLY_ON(BrowserThread::UI, base::NotFatalUntil::M152);
  if (!MediaStreamManager::IsOriginAllowed(render_frame_host_id.child_id,
                                           salt_and_origin.origin())) {
    // In this case, it's likely a navigation has occurred while processing this
    // request.
    std::move(cb).Run(MediaDeviceSaltAndOrigin::Empty(), false);
    return;
  }

  // Check that MediaStream device permissions have been granted for
  // nondefault devices.
  if (override_permissions) {
    std::move(cb).Run(salt_and_origin, permissions_override_value);
    return;
  }

  std::move(cb).Run(
      salt_and_origin,
      MediaDevicesPermissionChecker().CheckPermissionOnUIThread(
          MediaDeviceType::kMediaAudioOutput, render_frame_host_id));
}

// Returns (by callback) the MediaDeviceSaltAndOrigin for the frame and
// whether it may request nondefault audio devices.
void CheckAccessOnUIThread(
    GlobalRenderFrameHostId render_frame_host_id,
    bool override_permissions,
    bool permissions_override_value,
    base::OnceCallback<void(MediaDeviceSaltAndOrigin, bool)> cb) {
  CHECK_CURRENTLY_ON(BrowserThread::UI, base::NotFatalUntil::M152);
  GetMediaDeviceSaltAndOrigin(
      render_frame_host_id,
      base::BindOnce(&GotSaltAndOrigin, render_frame_host_id,
                     override_permissions, permissions_override_value,
                     std::move(cb)));
}

}  // namespace

class AudioOutputAuthorizationHandler::TraceScope {
 public:
  explicit TraceScope(const std::string& device_id)
      : trace_track_(perfetto::NamedTrack::FromPointer(
            "content::AudioOutputAuthorizationHandler",
            this)) {
    TRACE_EVENT_BEGIN("audio", "Audio output device authorization",
                      trace_track_);
    TRACE_EVENT_BEGIN("audio", "Request for device", trace_track_, "device id",
                      device_id);
  }

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

  ~TraceScope() {
    if (waiting_for_params_) {
      // End "Getting audio parameters" trace event.
      TRACE_EVENT_END("audio", trace_track_, "cancelled", true);
    }
    if (checking_access_) {
      // End "Checking access" trace event.
      TRACE_EVENT_END("audio", trace_track_, "cancelled", true);
    }
    // End "Request for device" trace event.
    TRACE_EVENT_END("audio", trace_track_);
    // End "Audio output device authorization" trace event.
    TRACE_EVENT_END("audio", trace_track_);
  }

  void SimpleEvent(perfetto::StaticString event) {
    TRACE_EVENT_INSTANT("audio", event, trace_track_);
  }

  void UsingSessionId(const base::UnguessableToken& session_id,
                      const std::string& device_id) {
    TRACE_EVENT_INSTANT("audio", "Using session id", trace_track_, "session id",
                        session_id.ToString(), "device id", device_id);
  }

  void CheckAccessStart(const std::string& device_id) {
    checking_access_ = true;
    TRACE_EVENT_BEGIN("audio", "Checking access", trace_track_, "device id",
                      device_id);
  }

  void AccessChecked(bool has_access) {
    checking_access_ = false;
    // End "Checking access" trace event.
    TRACE_EVENT_END("audio", trace_track_, "access granted", has_access);
  }

  void StartedGettingAudioParameters(const std::string& raw_device_id) {
    waiting_for_params_ = true;
    TRACE_EVENT_BEGIN("audio", "Getting audio parameters", trace_track_,
                      "device id", raw_device_id);
  }

  void FinishedGettingAudioParameters() {
    waiting_for_params_ = false;
    // End "Getting audio parameters" trace event.
    TRACE_EVENT_END("audio", trace_track_);
  }

 private:
  bool checking_access_ = false;
  bool waiting_for_params_ = false;
  const perfetto::NamedTrack trace_track_;
};

AudioOutputAuthorizationHandler::AudioOutputAuthorizationHandler(
    media::AudioSystem* audio_system,
    MediaStreamManager* media_stream_manager,
    GlobalRenderFrameHostId render_frame_host_id)
    : audio_system_(audio_system),
      media_stream_manager_(media_stream_manager),
      render_frame_host_id_(render_frame_host_id) {
  CHECK(media_stream_manager_, base::NotFatalUntil::M152);
}

AudioOutputAuthorizationHandler::~AudioOutputAuthorizationHandler() {
  // |weak_factory| is not thread safe. Make sure it's destructed on the
  // right thread.
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M152);
}

void AudioOutputAuthorizationHandler::RequestDeviceAuthorization(
    const base::UnguessableToken& session_id,
    const std::string& device_id,
    AuthorizationCompletedCallback cb) const {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M152);
  auto trace_scope = std::make_unique<TraceScope>(device_id);

  // If |session_id| should be used for output device selection and such an
  // output device is found, reuse the input device permissions.
  if (media::AudioDeviceDescription::UseSessionIdToSelectDevice(session_id,
                                                                device_id)) {
    if (!media_stream_manager_->ValidateAudioSession(session_id,
                                                     render_frame_host_id_)) {
      trace_scope->SimpleEvent("Unauthorized session");
      std::move(cb).Run(media::OUTPUT_DEVICE_STATUS_ERROR_NOT_AUTHORIZED,
                        media::AudioParameters::UnavailableDeviceParams(),
                        std::string(), std::string());
      return;
    }

    const blink::MediaStreamDevice* device =
        media_stream_manager_->audio_input_device_manager()
            ->GetOpenedDeviceById(session_id);
    if (device && device->matched_output_device_id) {
      trace_scope->UsingSessionId(session_id, device->id);
      // We don't need the origin for authorization in this case, but it's used
      // for hashing the device id before sending it back to the renderer.
      GetUIThreadTaskRunner({})->PostTask(
          FROM_HERE,
          base::BindOnce(
              &GetMediaDeviceSaltAndOrigin, render_frame_host_id_,
              base::BindPostTaskToCurrentDefault(base::BindOnce(
                  &AudioOutputAuthorizationHandler::HashDeviceId,
                  weak_factory_.GetWeakPtr(), std::move(trace_scope),
                  std::move(cb), *device->matched_output_device_id))));
      return;
    }
    // Otherwise, the default device is used.
  }

  if (media::AudioDeviceDescription::IsDefaultDevice(device_id)) {
    // The default device doesn't need authorization.
    GetDeviceParameters(std::move(trace_scope), std::move(cb),
                        media::AudioDeviceDescription::kDefaultDeviceId);
    return;
  }

  trace_scope->CheckAccessStart(device_id);
  // Check device permissions if nondefault device is requested.
  GetUIThreadTaskRunner({})->PostTask(
      FROM_HERE,
      base::BindOnce(&CheckAccessOnUIThread, render_frame_host_id_,
                     override_permissions_, permissions_override_value_,
                     base::BindPostTaskToCurrentDefault(base::BindOnce(
                         &AudioOutputAuthorizationHandler::AccessChecked,
                         weak_factory_.GetWeakPtr(), std::move(trace_scope),
                         std::move(cb), device_id))));
}

void AudioOutputAuthorizationHandler::OverridePermissionsForTesting(
    bool override_value) {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M152);
  override_permissions_ = true;
  permissions_override_value_ = override_value;
}

void AudioOutputAuthorizationHandler::
    SetAuthorizedDeviceIdForGlobalMediaControls(std::string hashed_device_id) {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M152);
  hashed_device_id_for_global_media_controls_ = std::move(hashed_device_id);
}

void AudioOutputAuthorizationHandler::UMALogDeviceAuthorizationTime(
    base::TimeTicks auth_start_time) {
  UMA_HISTOGRAM_CUSTOM_TIMES("Media.Audio.OutputDeviceAuthorizationTime",
                             base::TimeTicks::Now() - auth_start_time,
                             base::Milliseconds(1), base::Milliseconds(5000),
                             50);
}

void AudioOutputAuthorizationHandler::HashDeviceId(
    std::unique_ptr<TraceScope> trace_scope,
    AuthorizationCompletedCallback cb,
    const std::string& raw_device_id,
    const MediaDeviceSaltAndOrigin& salt_and_origin) const {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M152);
  CHECK(!raw_device_id.empty(), base::NotFatalUntil::M152);
  std::string hashed_device_id =
      GetHMACForRawMediaDeviceID(salt_and_origin, raw_device_id);
  trace_scope->StartedGettingAudioParameters(raw_device_id);
  audio_system_->GetOutputStreamParameters(
      raw_device_id,
      base::BindOnce(&AudioOutputAuthorizationHandler::DeviceParametersReceived,
                     weak_factory_.GetWeakPtr(), std::move(trace_scope),
                     std::move(cb), hashed_device_id, raw_device_id));
}

void AudioOutputAuthorizationHandler::AccessChecked(
    std::unique_ptr<TraceScope> trace_scope,
    AuthorizationCompletedCallback cb,
    const std::string& device_id,
    MediaDeviceSaltAndOrigin salt_and_origin,
    bool has_access) const {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M152);
  trace_scope->AccessChecked(has_access);

  // If this device has been explicitly allowed by the browser, overwrite the
  // result of the permission check.
  has_access = device_id == hashed_device_id_for_global_media_controls_
                   ? true
                   : has_access;

  if (has_access && !IsValidDeviceId(device_id)) {
    trace_scope->SimpleEvent("Invalid device id");
    std::move(cb).Run(media::OUTPUT_DEVICE_STATUS_ERROR_NOT_FOUND,
                      media::AudioParameters::UnavailableDeviceParams(),
                      std::string(), std::string());
    return;
  }

  if (!has_access) {
    std::move(cb).Run(media::OUTPUT_DEVICE_STATUS_ERROR_NOT_AUTHORIZED,
                      media::AudioParameters::UnavailableDeviceParams(),
                      std::string(), std::string());
    return;
  }

  MediaDevicesManager::BoolDeviceTypes devices_to_enumerate;
  devices_to_enumerate[static_cast<size_t>(
      MediaDeviceType::kMediaAudioOutput)] = true;
  media_stream_manager_->media_devices_manager()->EnumerateDevices(
      devices_to_enumerate,
      base::BindOnce(&AudioOutputAuthorizationHandler::TranslateDeviceID,
                     weak_factory_.GetWeakPtr(), std::move(trace_scope),
                     std::move(cb), device_id, std::move(salt_and_origin)));
}

void AudioOutputAuthorizationHandler::TranslateDeviceID(
    std::unique_ptr<TraceScope> trace_scope,
    AuthorizationCompletedCallback cb,
    const std::string& device_id,
    const MediaDeviceSaltAndOrigin& salt_and_origin,
    const MediaDeviceEnumeration& enumeration) const {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M152);
  CHECK(!media::AudioDeviceDescription::IsDefaultDevice(device_id),
        base::NotFatalUntil::M152);

  for (const blink::WebMediaDeviceInfo& device_info :
       enumeration[static_cast<size_t>(MediaDeviceType::kMediaAudioOutput)]) {
    if (DoesRawMediaDeviceIDMatchHMAC(salt_and_origin, device_id,
                                      device_info.device_id)) {
      GetDeviceParameters(std::move(trace_scope), std::move(cb),
                          device_info.device_id);
      return;
    }
  }

  trace_scope->SimpleEvent("Found no device matching device id");
  std::move(cb).Run(media::OUTPUT_DEVICE_STATUS_ERROR_NOT_FOUND,
                    media::AudioParameters::UnavailableDeviceParams(),
                    std::string(), std::string());
}

void AudioOutputAuthorizationHandler::GetDeviceParameters(
    std::unique_ptr<TraceScope> trace_scope,
    AuthorizationCompletedCallback cb,
    const std::string& raw_device_id) const {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M152);
  CHECK(!raw_device_id.empty(), base::NotFatalUntil::M152);

  trace_scope->StartedGettingAudioParameters(raw_device_id);
  audio_system_->GetOutputStreamParameters(
      raw_device_id,
      base::BindOnce(&AudioOutputAuthorizationHandler::DeviceParametersReceived,
                     weak_factory_.GetWeakPtr(), std::move(trace_scope),
                     std::move(cb), std::string(), raw_device_id));
}

void AudioOutputAuthorizationHandler::DeviceParametersReceived(
    std::unique_ptr<TraceScope> trace_scope,
    AuthorizationCompletedCallback cb,
    const std::string& id_for_renderer,
    const std::string& raw_device_id,
    const std::optional<media::AudioParameters>& params) const {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M152);
  CHECK(!raw_device_id.empty(), base::NotFatalUntil::M152);
  CHECK(!params || params->IsValid(), base::NotFatalUntil::M152);

  trace_scope->FinishedGettingAudioParameters();

  std::move(cb).Run(
      media::OUTPUT_DEVICE_STATUS_OK,
      params.value_or(media::AudioParameters::UnavailableDeviceParams()),
      raw_device_id, id_for_renderer);
}

}  // namespace content
