chromium/ios/chrome/browser/permissions/model/media_api_usage_java_script_feature.mm

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

#import "ios/chrome/browser/permissions/model/media_api_usage_java_script_feature.h"

#import <AVFAudio/AVFAudio.h>

#import "base/metrics/histogram_functions.h"
#import "ios/web/public/js_messaging/script_message.h"

namespace {

// These values are logged to UMA. Entries should not be renumbered and
// numeric values should never be reused. Please keep in sync with
// "MediaAPIParams" in src/tools/metrics/histograms/enums.xml.
enum class MediaAPIParams {
  kUnknown = 0,
  kAudioOnly = 1,
  kVideoOnly = 2,
  kAudioAndVideo = 3,
  kMaxValue = kAudioAndVideo,
};

static constexpr char kScriptName[] = "media_overrides";

static constexpr char kMediaAPIAccessedHandlerName[] =
    "MediaAPIAccessedHandler";

static constexpr char kScriptMessageResponseAudioKey[] = "audio";
static constexpr char kScriptMessageResponseVideoKey[] = "video";

static constexpr char kMediaAPIAccessedHistogramDenied[] =
    "IOS.JavaScript.Permissions.Media.Denied";
static constexpr char kMediaAPIAccessedHistogramGranted[] =
    "IOS.JavaScript.Permissions.Media.Granted";
static constexpr char kMediaAPIAccessedHistogramUndetermined[] =
    "IOS.JavaScript.Permissions.Media.Undetermined";

}  // namespace

// static
MediaAPIUsageJavaScriptFeature* MediaAPIUsageJavaScriptFeature::GetInstance() {
  static base::NoDestructor<MediaAPIUsageJavaScriptFeature> instance;
  return instance.get();
}

// static
bool MediaAPIUsageJavaScriptFeature::ShouldOverrideAPI() {
  // Install JS overrides if access is `...Undetermined` or `...Denied`.
  if (@available(iOS 17.0, *)) {
    return [AVAudioApplication sharedInstance].recordPermission !=
           AVAudioApplicationRecordPermissionGranted;
  }
  return [AVAudioSession sharedInstance].recordPermission !=
         AVAudioSessionRecordPermissionGranted;
}

MediaAPIUsageJavaScriptFeature::MediaAPIUsageJavaScriptFeature()
    : JavaScriptFeature(web::ContentWorld::kPageContentWorld,
                        {FeatureScript::CreateWithFilename(
                            kScriptName,
                            FeatureScript::InjectionTime::kDocumentStart,
                            FeatureScript::TargetFrames::kAllFrames,
                            FeatureScript::ReinjectionBehavior::
                                kReinjectOnDocumentRecreation)}) {}
MediaAPIUsageJavaScriptFeature::~MediaAPIUsageJavaScriptFeature() = default;

std::optional<std::string>
MediaAPIUsageJavaScriptFeature::GetScriptMessageHandlerName() const {
  return kMediaAPIAccessedHandlerName;
}

void MediaAPIUsageJavaScriptFeature::ScriptMessageReceived(
    web::WebState* web_state,
    const web::ScriptMessage& script_message) {
  std::optional<bool> audio;
  std::optional<bool> video;
  const base::Value::Dict* script_dict =
      script_message.body() ? script_message.body()->GetIfDict() : nullptr;
  if (script_dict) {
    audio = script_dict->FindBool(kScriptMessageResponseAudioKey);
    video = script_dict->FindBool(kScriptMessageResponseVideoKey);
  }

  std::string metric_name;
  if (@available(iOS 17.0, *)) {
    switch ([AVAudioApplication sharedInstance].recordPermission) {
      case AVAudioApplicationRecordPermissionDenied:
        metric_name = kMediaAPIAccessedHistogramDenied;
        break;
      case AVAudioApplicationRecordPermissionGranted:
        metric_name = kMediaAPIAccessedHistogramGranted;
        break;
      case AVAudioApplicationRecordPermissionUndetermined:
        metric_name = kMediaAPIAccessedHistogramUndetermined;
        break;
    }
  } else {
    switch ([AVAudioSession sharedInstance].recordPermission) {
      case AVAudioSessionRecordPermissionDenied:
        metric_name = kMediaAPIAccessedHistogramDenied;
        break;
      case AVAudioSessionRecordPermissionGranted:
        metric_name = kMediaAPIAccessedHistogramGranted;
        break;
      case AVAudioSessionRecordPermissionUndetermined:
        metric_name = kMediaAPIAccessedHistogramUndetermined;
        break;
    }
  }

  if (!audio || !video) {
    base::UmaHistogramEnumeration(metric_name, MediaAPIParams::kUnknown);
  } else if (audio.value() && !video.value()) {
    base::UmaHistogramEnumeration(metric_name, MediaAPIParams::kAudioOnly);
  } else if (!audio.value() && video.value()) {
    base::UmaHistogramEnumeration(metric_name, MediaAPIParams::kVideoOnly);
  } else if (audio.value() && video.value()) {
    base::UmaHistogramEnumeration(metric_name, MediaAPIParams::kAudioAndVideo);
  }
}