chromium/components/safe_browsing/android/safe_browsing_api_handler_bridge.cc

// 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 "components/safe_browsing/android/safe_browsing_api_handler_bridge.h"

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

#include "base/android/jni_array.h"
#include "base/android/jni_string.h"
#include "base/command_line.h"
#include "base/containers/contains.h"
#include "base/containers/flat_set.h"
#include "base/containers/heap_array.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/string_tokenizer.h"
#include "base/time/time.h"
#include "base/trace_event/trace_event.h"
#include "base/types/fixed_array.h"
#include "components/safe_browsing/android/safe_browsing_api_handler_util.h"
#include "components/safe_browsing/core/browser/db/v4_protocol_manager_util.h"
#include "components/safe_browsing/core/common/features.h"
#include "components/safe_browsing/core/common/safebrowsing_switches.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"

// Must come after all headers that specialize FromJniType() / ToJniType().
#include "components/safe_browsing/android/jni_headers/SafeBrowsingApiBridge_jni.h"

using base::android::AttachCurrentThread;
using base::android::ConvertJavaStringToUTF8;
using base::android::ConvertUTF8ToJavaString;
using base::android::JavaIntArrayToIntVector;
using base::android::JavaParamRef;
using base::android::ScopedJavaLocalRef;
using base::android::ToJavaIntArray;
using content::BrowserThread;

namespace safe_browsing {

namespace {

void ReportUmaResult(UmaRemoteCallResult result) {
  UMA_HISTOGRAM_ENUMERATION("SB2.RemoteCall.Result", result,
                            UmaRemoteCallResult::MAX_VALUE);
}

std::string GetSafeBrowsingJavaProtocolUmaSuffix(
    SafeBrowsingJavaProtocol protocol) {
  switch (protocol) {
    case SafeBrowsingJavaProtocol::LOCAL_BLOCK_LIST:
      return ".LocalBlocklist";
    case SafeBrowsingJavaProtocol::REAL_TIME:
      return ".RealTime";
  }
}

void ReportSafeBrowsingJavaValidationResult(
    SafeBrowsingJavaProtocol protocol,
    SafeBrowsingJavaValidationResult validation_result) {
  base::UmaHistogramEnumeration(
      "SafeBrowsing.GmsSafeBrowsingApi.JavaValidationResult",
      validation_result);
  base::UmaHistogramEnumeration(
      "SafeBrowsing.GmsSafeBrowsingApi.JavaValidationResult" +
          GetSafeBrowsingJavaProtocolUmaSuffix(protocol),
      validation_result);
}

void ReportUmaHistogramSparseWithAndWithoutSuffix(const std::string& metric,
                                                  const std::string& suffix,
                                                  int value) {
  base::UmaHistogramSparse(metric, value);
  base::UmaHistogramSparse(metric + suffix, value);
}

void ReportSafeBrowsingJavaResponse(
    SafeBrowsingJavaProtocol protocol,
    SafeBrowsingApiLookupResult lookup_result,
    SafeBrowsingJavaThreatType threat_type,
    const std::vector<int>& threat_attributes,
    SafeBrowsingJavaResponseStatus response_status,
    jlong check_delta_microseconds) {
  std::string suffix = GetSafeBrowsingJavaProtocolUmaSuffix(protocol);

  base::UmaHistogramMicrosecondsTimes(
      "SafeBrowsing.GmsSafeBrowsingApi.CheckDelta",
      base::Microseconds(check_delta_microseconds));
  base::UmaHistogramMicrosecondsTimes(
      "SafeBrowsing.GmsSafeBrowsingApi.CheckDelta" + suffix,
      base::Microseconds(check_delta_microseconds));

  ReportUmaHistogramSparseWithAndWithoutSuffix(
      "SafeBrowsing.GmsSafeBrowsingApi.LookupResult", suffix,
      static_cast<int>(lookup_result));
  if (lookup_result != SafeBrowsingApiLookupResult::SUCCESS) {
    // Do not log other histograms if the lookup failed, since the other values
    // will all be dummy values.
    return;
  }
  ReportUmaHistogramSparseWithAndWithoutSuffix(
      "SafeBrowsing.GmsSafeBrowsingApi.ThreatType2", suffix,
      static_cast<int>(threat_type));
  base::UmaHistogramCounts100(
      "SafeBrowsing.GmsSafeBrowsingApi.ThreatAttributeCount",
      threat_attributes.size());
  base::UmaHistogramCounts100(
      "SafeBrowsing.GmsSafeBrowsingApi.ThreatAttributeCount" + suffix,
      threat_attributes.size());
  for (int threat_attribute : threat_attributes) {
    ReportUmaHistogramSparseWithAndWithoutSuffix(
        "SafeBrowsing.GmsSafeBrowsingApi.ThreatAttribute", suffix,
        threat_attribute);
  }
  ReportUmaHistogramSparseWithAndWithoutSuffix(
      "SafeBrowsing.GmsSafeBrowsingApi.ResponseStatus", suffix,
      static_cast<int>(response_status));

  if (response_status ==
      SafeBrowsingJavaResponseStatus::SUCCESS_WITH_REAL_TIME) {
    base::UmaHistogramMicrosecondsTimes(
        "SafeBrowsing.GmsSafeBrowsingApi.CheckDelta.SuccessWithRealTime",
        base::Microseconds(check_delta_microseconds));
  }
}

SafeBrowsingJavaValidationResult GetJavaValidationResult(
    SafeBrowsingApiLookupResult lookup_result,
    SafeBrowsingJavaThreatType threat_type,
    const std::vector<int>& threat_attributes,
    SafeBrowsingJavaResponseStatus response_status) {
  bool is_lookup_result_recognized = false;
  switch (lookup_result) {
    case SafeBrowsingApiLookupResult::SUCCESS:
    case SafeBrowsingApiLookupResult::FAILURE:
    case SafeBrowsingApiLookupResult::FAILURE_API_CALL_TIMEOUT:
    case SafeBrowsingApiLookupResult::FAILURE_API_UNSUPPORTED:
    case SafeBrowsingApiLookupResult::FAILURE_API_NOT_AVAILABLE:
    case SafeBrowsingApiLookupResult::FAILURE_HANDLER_NULL:
      is_lookup_result_recognized = true;
      break;
  }
  if (!is_lookup_result_recognized) {
    return SafeBrowsingJavaValidationResult::INVALID_LOOKUP_RESULT;
  }

  bool is_threat_type_recognized = false;
  switch (threat_type) {
    case SafeBrowsingJavaThreatType::NO_THREAT:
    case SafeBrowsingJavaThreatType::SOCIAL_ENGINEERING:
    case SafeBrowsingJavaThreatType::UNWANTED_SOFTWARE:
    case SafeBrowsingJavaThreatType::POTENTIALLY_HARMFUL_APPLICATION:
    case SafeBrowsingJavaThreatType::BILLING:
    case SafeBrowsingJavaThreatType::ABUSIVE_EXPERIENCE_VIOLATION:
    case SafeBrowsingJavaThreatType::BETTER_ADS_VIOLATION:
      is_threat_type_recognized = true;
      break;
  }
  if (!is_threat_type_recognized) {
    return SafeBrowsingJavaValidationResult::INVALID_THREAT_TYPE;
  }

  for (int threat_attribute : threat_attributes) {
    SafeBrowsingJavaThreatAttribute threat_attribute_enum =
        static_cast<SafeBrowsingJavaThreatAttribute>(threat_attribute);
    bool is_threat_attribute_recognized = false;
    switch (threat_attribute_enum) {
      case SafeBrowsingJavaThreatAttribute::CANARY:
      case SafeBrowsingJavaThreatAttribute::FRAME_ONLY:
        is_threat_attribute_recognized = true;
        break;
    }
    if (!is_threat_attribute_recognized) {
      return SafeBrowsingJavaValidationResult::INVALID_THREAT_ATTRIBUTE;
    }
  }

  bool is_reponse_status_recognized = false;
  switch (response_status) {
    case SafeBrowsingJavaResponseStatus::SUCCESS_WITH_LOCAL_BLOCKLIST:
    case SafeBrowsingJavaResponseStatus::SUCCESS_WITH_REAL_TIME:
    case SafeBrowsingJavaResponseStatus::SUCCESS_FALLBACK_REAL_TIME_TIMEOUT:
    case SafeBrowsingJavaResponseStatus::SUCCESS_FALLBACK_REAL_TIME_THROTTLED:
    case SafeBrowsingJavaResponseStatus::FAILURE_NETWORK_UNAVAILABLE:
    case SafeBrowsingJavaResponseStatus::FAILURE_BLOCK_LIST_UNAVAILABLE:
    case SafeBrowsingJavaResponseStatus::FAILURE_INVALID_URL:
      is_reponse_status_recognized = true;
      break;
  }
  if (!is_reponse_status_recognized) {
    return SafeBrowsingJavaValidationResult::
        VALID_WITH_UNRECOGNIZED_RESPONSE_STATUS;
  }

  return SafeBrowsingJavaValidationResult::VALID;
}

// Validate the values returned from SafeBrowsing API are defined in enum. The
// response can be out of range if there is version mismatch between Chrome and
// the GMSCore APK, or the enums between c++ and java are not aligned.
bool IsResponseFromJavaValid(SafeBrowsingJavaProtocol protocol,
                             SafeBrowsingApiLookupResult lookup_result,
                             SafeBrowsingJavaThreatType threat_type,
                             const std::vector<int>& threat_attributes,
                             SafeBrowsingJavaResponseStatus response_status) {
  SafeBrowsingJavaValidationResult validation_result = GetJavaValidationResult(
      lookup_result, threat_type, threat_attributes, response_status);
  ReportSafeBrowsingJavaValidationResult(protocol, validation_result);

  switch (validation_result) {
    case SafeBrowsingJavaValidationResult::VALID:
    // Not returning false if response_status is unrecognized. This is to avoid
    // the API adding a new success response_status while we haven't integrated
    // the new value yet. In this case, we still want to return the threat_type.
    case SafeBrowsingJavaValidationResult::
        VALID_WITH_UNRECOGNIZED_RESPONSE_STATUS:
      return true;
    case SafeBrowsingJavaValidationResult::INVALID_LOOKUP_RESULT:
    case SafeBrowsingJavaValidationResult::INVALID_THREAT_TYPE:
    case SafeBrowsingJavaValidationResult::INVALID_THREAT_ATTRIBUTE:
      return false;
  }
}

bool IsLookupSuccessful(SafeBrowsingApiLookupResult lookup_result,
                        SafeBrowsingJavaResponseStatus response_status) {
  bool is_lookup_result_success = false;
  switch (lookup_result) {
    case SafeBrowsingApiLookupResult::SUCCESS:
      is_lookup_result_success = true;
      break;
    case SafeBrowsingApiLookupResult::FAILURE:
    case SafeBrowsingApiLookupResult::FAILURE_API_CALL_TIMEOUT:
    case SafeBrowsingApiLookupResult::FAILURE_API_UNSUPPORTED:
    case SafeBrowsingApiLookupResult::FAILURE_API_NOT_AVAILABLE:
    case SafeBrowsingApiLookupResult::FAILURE_HANDLER_NULL:
      break;
  }
  if (!is_lookup_result_success) {
    return false;
  }

  // Note that we check explicit failure statuses instead of success statuses.
  // This is to avoid the API adding a new success response_status while we
  // haven't integrated the new value yet. The impact of a missing failure
  // status is smaller since the API is expected to return a safe threat type in
  // a failure anyway.
  bool is_response_status_success = true;
  switch (response_status) {
    case SafeBrowsingJavaResponseStatus::SUCCESS_WITH_LOCAL_BLOCKLIST:
    case SafeBrowsingJavaResponseStatus::SUCCESS_WITH_REAL_TIME:
    case SafeBrowsingJavaResponseStatus::SUCCESS_FALLBACK_REAL_TIME_TIMEOUT:
    case SafeBrowsingJavaResponseStatus::SUCCESS_FALLBACK_REAL_TIME_THROTTLED:
      break;
    case SafeBrowsingJavaResponseStatus::FAILURE_NETWORK_UNAVAILABLE:
    case SafeBrowsingJavaResponseStatus::FAILURE_BLOCK_LIST_UNAVAILABLE:
    case SafeBrowsingJavaResponseStatus::FAILURE_INVALID_URL:
      is_response_status_success = false;
      break;
  }
  return is_response_status_success;
}

bool IsSafeBrowsingNonRecoverable(SafeBrowsingApiLookupResult lookup_result) {
  switch (lookup_result) {
    case SafeBrowsingApiLookupResult::FAILURE_API_UNSUPPORTED:
    case SafeBrowsingApiLookupResult::FAILURE_API_NOT_AVAILABLE:
    case SafeBrowsingApiLookupResult::FAILURE_HANDLER_NULL:
      return true;
    case SafeBrowsingApiLookupResult::SUCCESS:
    case SafeBrowsingApiLookupResult::FAILURE:
    case SafeBrowsingApiLookupResult::FAILURE_API_CALL_TIMEOUT:
      return false;
  }
}

// Convert a SBThreatType to a Java SafetyNet API threat type.  We only support
// a few.
SafetyNetJavaThreatType SBThreatTypeToSafetyNetJavaThreatType(
    const SBThreatType& sb_threat_type) {
  using enum SBThreatType;
  switch (sb_threat_type) {
    case SB_THREAT_TYPE_BILLING:
      return SafetyNetJavaThreatType::BILLING;
    case SB_THREAT_TYPE_SUBRESOURCE_FILTER:
      return SafetyNetJavaThreatType::SUBRESOURCE_FILTER;
    case SB_THREAT_TYPE_URL_PHISHING:
      return SafetyNetJavaThreatType::SOCIAL_ENGINEERING;
    case SB_THREAT_TYPE_URL_MALWARE:
      return SafetyNetJavaThreatType::POTENTIALLY_HARMFUL_APPLICATION;
    case SB_THREAT_TYPE_URL_UNWANTED:
      return SafetyNetJavaThreatType::UNWANTED_SOFTWARE;
    case SB_THREAT_TYPE_CSD_ALLOWLIST:
      return SafetyNetJavaThreatType::CSD_ALLOWLIST;
    default:
      NOTREACHED_IN_MIGRATION();
      return SafetyNetJavaThreatType::MAX_VALUE;
  }
}

// Convert a vector of SBThreatTypes to JavaIntArray of Java SafetyNet API
// threat types.
ScopedJavaLocalRef<jintArray> SBThreatTypeSetToSafetyNetJavaArray(
    JNIEnv* env,
    const SBThreatTypeSet& threat_types) {
  DCHECK_LT(0u, threat_types.size());
  auto int_threat_types = base::HeapArray<int>::WithSize(threat_types.size());
  auto itr = int_threat_types.begin();
  for (auto threat_type : threat_types) {
    *itr++ =
        static_cast<int>(SBThreatTypeToSafetyNetJavaThreatType(threat_type));
  }
  return ToJavaIntArray(env, int_threat_types);
}

// Convert a Java threat type for SafeBrowsing to a SBThreatType.
SBThreatType SafeBrowsingJavaToSBThreatType(
    SafeBrowsingJavaThreatType java_threat_num) {
  using enum SBThreatType;
  switch (java_threat_num) {
    case SafeBrowsingJavaThreatType::NO_THREAT:
      return SB_THREAT_TYPE_SAFE;
    case SafeBrowsingJavaThreatType::SOCIAL_ENGINEERING:
      return SB_THREAT_TYPE_URL_PHISHING;
    case SafeBrowsingJavaThreatType::UNWANTED_SOFTWARE:
      return SB_THREAT_TYPE_URL_UNWANTED;
    case SafeBrowsingJavaThreatType::POTENTIALLY_HARMFUL_APPLICATION:
      return SB_THREAT_TYPE_URL_MALWARE;
    case SafeBrowsingJavaThreatType::BILLING:
      return SB_THREAT_TYPE_BILLING;
    case SafeBrowsingJavaThreatType::ABUSIVE_EXPERIENCE_VIOLATION:
    case SafeBrowsingJavaThreatType::BETTER_ADS_VIOLATION:
      return SB_THREAT_TYPE_SUBRESOURCE_FILTER;
  }
}

// Convert a SBThreatType to a Java threat type for SafeBrowsing. We only
// support a few.
SafeBrowsingJavaThreatType SBThreatTypeToSafeBrowsingApiJavaThreatType(
    const SBThreatType& sb_threat_type) {
  using enum SBThreatType;
  switch (sb_threat_type) {
    case SB_THREAT_TYPE_URL_PHISHING:
      return SafeBrowsingJavaThreatType::SOCIAL_ENGINEERING;
    case SB_THREAT_TYPE_URL_UNWANTED:
      return SafeBrowsingJavaThreatType::UNWANTED_SOFTWARE;
    case SB_THREAT_TYPE_URL_MALWARE:
      return SafeBrowsingJavaThreatType::POTENTIALLY_HARMFUL_APPLICATION;
    case SB_THREAT_TYPE_BILLING:
      return SafeBrowsingJavaThreatType::BILLING;
    default:
      NOTREACHED_IN_MIGRATION();
      return SafeBrowsingJavaThreatType::NO_THREAT;
  }
}

// Convert a vector of SBThreatTypes to JavaIntArray of SafeBrowsing API's
// threat types.
ScopedJavaLocalRef<jintArray> SBThreatTypeSetToSafeBrowsingJavaArray(
    JNIEnv* env,
    const SBThreatTypeSet& threat_types) {
  DCHECK_LT(0u, threat_types.size());
  size_t threat_type_size =
      base::Contains(threat_types,
                     SBThreatType::SB_THREAT_TYPE_SUBRESOURCE_FILTER)
          ? threat_types.size() + 1
          : threat_types.size();
  auto int_threat_types = base::HeapArray<int>::WithSize(threat_type_size);
  auto itr = int_threat_types.begin();
  for (auto threat_type : threat_types) {
    if (threat_type == SBThreatType::SB_THREAT_TYPE_SUBRESOURCE_FILTER) {
      *itr++ = static_cast<int>(
          SafeBrowsingJavaThreatType::ABUSIVE_EXPERIENCE_VIOLATION);
      *itr++ =
          static_cast<int>(SafeBrowsingJavaThreatType::BETTER_ADS_VIOLATION);
    } else {
      *itr++ = static_cast<int>(
          SBThreatTypeToSafeBrowsingApiJavaThreatType(threat_type));
    }
  }
  return ToJavaIntArray(env, int_threat_types);
}

// The map that holds the callback_id used to reference each pending SafetyNet
// request sent to Java, and the corresponding callback to call on receiving the
// response.
using PendingSafetyNetCallbacksMap =
    std::unordered_map<jlong, SafeBrowsingApiHandlerBridge::ResponseCallback>;

PendingSafetyNetCallbacksMap& GetPendingSafetyNetCallbacksMap() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  // Holds the list of callback objects that we are currently waiting to hear
  // the result of from GmsCore.
  // The key is a unique count-up integer.
  static base::NoDestructor<PendingSafetyNetCallbacksMap>
      pending_safety_net_callbacks;
  return *pending_safety_net_callbacks;
}

// Customized struct to hold a callback to the SafeBrowsing API and the protocol
// used to make that call. The protocol is stored for histogram logging.
struct SafeBrowsingResponseCallback {
  SafeBrowsingJavaProtocol protocol;
  SafeBrowsingApiHandlerBridge::ResponseCallback response_callback;
};

// The map that holds the callback_id used to reference each pending
// SafeBrowsing request sent to Java, and the corresponding callback to call on
// receiving the response.
using PendingSafeBrowsingCallbacksMap =
    std::unordered_map<jlong, SafeBrowsingResponseCallback>;

PendingSafeBrowsingCallbacksMap& GetPendingSafeBrowsingCallbacksMap() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  // Holds the list of callback objects that we are currently waiting to hear
  // the result of from GmsCore.
  // The key is a unique count-up integer.
  static base::NoDestructor<PendingSafeBrowsingCallbacksMap>
      pending_safe_browsing_callbacks;
  return *pending_safe_browsing_callbacks;
}

using PendingVerifyAppsCallbacksMap = std::unordered_map<
    jlong,
    SafeBrowsingApiHandlerBridge::VerifyAppsResponseCallback>;
PendingVerifyAppsCallbacksMap& GetPendingVerifyAppsCallbacks() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  // Holds the list of callback objects that we are currently waiting to hear
  // the result of from GmsCore.
  // The key is a unique count-up integer.
  static base::NoDestructor<PendingVerifyAppsCallbacksMap> pending_callbacks;
  return *pending_callbacks;
}

bool StartAllowlistCheck(const GURL& url, const SBThreatType& sb_threat_type) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  JNIEnv* env = AttachCurrentThread();
  if (!Java_SafeBrowsingApiBridge_ensureSafetyNetApiInitialized(env)) {
    return false;
  }

  ScopedJavaLocalRef<jstring> j_url = ConvertUTF8ToJavaString(env, url.spec());
  int j_threat_type =
      static_cast<int>(SBThreatTypeToSafetyNetJavaThreatType(sb_threat_type));
  return Java_SafeBrowsingApiBridge_startAllowlistLookup(env, j_url,
                                                         j_threat_type);
}

}  // namespace

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

// Respond to the URL reputation request by looking up the callback information
// stored in |pending_safety_net_callbacks|.
//   |callback_id| is an int form of pointer to a ::ResponseCallback
//                 that will be called and then deleted here.
//   |j_result_status| is one of those from SafeBrowsingApiHandlerBridge.java
//   |metadata| is a JSON string classifying the threat if there is one.
void OnUrlCheckDoneBySafetyNetApi(jlong callback_id,
                                  jint j_result_status,
                                  const std::string metadata) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  PendingSafetyNetCallbacksMap& pending_callbacks =
      GetPendingSafetyNetCallbacksMap();
  bool found = base::Contains(pending_callbacks, callback_id);
  DCHECK(found) << "Not found in pending_safety_net_callbacks: " << callback_id;
  if (!found)
    return;

  SafeBrowsingApiHandlerBridge::ResponseCallback callback =
      std::move(pending_callbacks[callback_id]);
  pending_callbacks.erase(callback_id);

  SafetyNetRemoteCallResultStatus result_status =
      static_cast<SafetyNetRemoteCallResultStatus>(j_result_status);
  if (result_status != SafetyNetRemoteCallResultStatus::SUCCESS) {
    if (result_status == SafetyNetRemoteCallResultStatus::TIMEOUT) {
      ReportUmaResult(UmaRemoteCallResult::TIMEOUT);
    } else {
      DCHECK_EQ(result_status, SafetyNetRemoteCallResultStatus::INTERNAL_ERROR);
      ReportUmaResult(UmaRemoteCallResult::INTERNAL_ERROR);
    }
    std::move(callback).Run(SBThreatType::SB_THREAT_TYPE_SAFE,
                            ThreatMetadata());
    return;
  }

  // Shortcut for safe, so we don't have to parse JSON.
  if (metadata == "{}") {
    ReportUmaResult(UmaRemoteCallResult::SAFE);
    std::move(callback).Run(SBThreatType::SB_THREAT_TYPE_SAFE,
                            ThreatMetadata());
  } else {
    // Unsafe, assuming we can parse the JSON.
    SBThreatType worst_threat;
    ThreatMetadata threat_metadata;
    ReportUmaResult(
        ParseJsonFromGMSCore(metadata, &worst_threat, &threat_metadata));

    std::move(callback).Run(worst_threat, threat_metadata);
  }
}

// Java->Native call, invoked when a SafetyNet check is done.
//   |callback_id| is a key into the |pending_safety_net_callbacks| map, whose
//   value is a ::ResponseCallback that will be called and then deleted on
//   the IO thread.
//   |result_status| is a @SafeBrowsingResult from SafetyNetApiHandler.java
//   |metadata| is a JSON string classifying the threat if there is one.
//   |check_delta| is the number of microseconds it took to look up the URL
//                 reputation from GmsCore.
//
//   Careful note: this can be called on multiple threads, so make sure there is
//   nothing thread unsafe happening here.
void JNI_SafeBrowsingApiBridge_OnUrlCheckDoneBySafetyNetApi(
    JNIEnv* env,
    jlong callback_id,
    jint result_status,
    const JavaParamRef<jstring>& metadata,
    jlong check_delta) {
  UMA_HISTOGRAM_COUNTS_10M("SB2.RemoteCall.CheckDelta", check_delta);

  const std::string metadata_str =
      (metadata ? ConvertJavaStringToUTF8(env, metadata) : "");

  TRACE_EVENT1("safe_browsing",
               "SafeBrowsingApiHandlerBridge::nUrlCheckDoneBySafetyNetApi",
               "metadata", metadata_str);

  content::GetUIThreadTaskRunner({})->PostTask(
      FROM_HERE, base::BindOnce(&OnUrlCheckDoneBySafetyNetApi, callback_id,
                                result_status, metadata_str));
}

// Respond to the URL reputation request by looking up the callback information
// stored in |pending_safe_browsing_callbacks|. Must be called on the original
// thread that starts the lookup.
void OnUrlCheckDoneBySafeBrowsingApi(
    jlong callback_id,
    SafeBrowsingApiLookupResult lookup_result,
    SafeBrowsingJavaThreatType threat_type,
    std::vector<int> threat_attributes,
    SafeBrowsingJavaResponseStatus response_status,
    jlong check_delta_microseconds) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  PendingSafeBrowsingCallbacksMap& pending_callbacks =
      GetPendingSafeBrowsingCallbacksMap();
  bool found = base::Contains(pending_callbacks, callback_id);
  DCHECK(found) << "Not found in pending_safe_browsing_callbacks: "
                << callback_id;
  if (!found) {
    return;
  }

  SafeBrowsingResponseCallback callback =
      std::move(pending_callbacks[callback_id]);
  pending_callbacks.erase(callback_id);

  ReportSafeBrowsingJavaResponse(callback.protocol, lookup_result, threat_type,
                                 threat_attributes, response_status,
                                 check_delta_microseconds);

  if (!IsResponseFromJavaValid(callback.protocol, lookup_result, threat_type,
                               threat_attributes, response_status)) {
    std::move(callback.response_callback)
        .Run(SBThreatType::SB_THREAT_TYPE_SAFE, ThreatMetadata());
    return;
  }

  if (!IsLookupSuccessful(lookup_result, response_status)) {
    if (IsSafeBrowsingNonRecoverable(lookup_result)) {
      SafeBrowsingApiHandlerBridge::GetInstance()
          .OnSafeBrowsingApiNonRecoverableFailure();
    }
    std::move(callback.response_callback)
        .Run(SBThreatType::SB_THREAT_TYPE_SAFE, ThreatMetadata());
    return;
  }

  std::move(callback.response_callback)
      .Run(
          SafeBrowsingJavaToSBThreatType(threat_type),
          GetThreatMetadataFromSafeBrowsingApi(threat_type, threat_attributes));
}

// Java->Native call, invoked when a SafeBrowsing check is done. |env| is the
// JNI environment that stores local pointers. |callback_id| is a key into the
// |pending_safe_browsing_callbacks| map, whose value is a ::ResponseCallback
// that will be called and then deleted on the IO thread. |j_lookup_result| is a
// @LookupResult from SafeBrowsingApiHandler.java. |j_threat_type| is the threat
// type that matched against the URL. |j_threat_attributes| is the threat
// attributes that matched against the URL. |j_response_status| reflects how the
// API gets the response. |check_delta_microseconds| is the number of
// microseconds it took to look up the URL reputation from GmsCore.
//
// Careful note: this can be called on multiple threads, so make sure there is
// nothing thread unsafe happening here.
void JNI_SafeBrowsingApiBridge_OnUrlCheckDoneBySafeBrowsingApi(
    JNIEnv* env,
    jlong callback_id,
    jint j_lookup_result,
    jint j_threat_type,
    const JavaParamRef<jintArray>& j_threat_attributes,
    jint j_response_status,
    jlong check_delta_microseconds) {
  SafeBrowsingApiLookupResult lookup_result =
      static_cast<SafeBrowsingApiLookupResult>(j_lookup_result);
  SafeBrowsingJavaThreatType threat_type =
      static_cast<SafeBrowsingJavaThreatType>(j_threat_type);
  std::vector<int> threat_attributes;
  JavaIntArrayToIntVector(env, j_threat_attributes, &threat_attributes);
  SafeBrowsingJavaResponseStatus response_status =
      static_cast<SafeBrowsingJavaResponseStatus>(j_response_status);
  content::GetUIThreadTaskRunner({})->PostTask(
      FROM_HERE,
      base::BindOnce(&OnUrlCheckDoneBySafeBrowsingApi, callback_id,
                     lookup_result, threat_type, std::move(threat_attributes),
                     response_status, check_delta_microseconds));
}

void OnVerifyAppsEnabledDone(jlong callback_id, jint j_result) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  PendingVerifyAppsCallbacksMap& pending_callbacks =
      GetPendingVerifyAppsCallbacks();
  bool found = base::Contains(pending_callbacks, callback_id);
  DCHECK(found) << "Not found in pending_verify_apps_callbacks: "
                << callback_id;
  if (!found) {
    return;
  }

  SafeBrowsingApiHandlerBridge::VerifyAppsResponseCallback callback =
      std::move(pending_callbacks[callback_id]);
  std::move(callback).Run(static_cast<VerifyAppsEnabledResult>(j_result));
}

void JNI_SafeBrowsingApiBridge_OnVerifyAppsEnabledDone(JNIEnv* env,
                                                       jlong callback_id,
                                                       jint j_result) {
  content::GetUIThreadTaskRunner({})->PostTask(
      FROM_HERE,
      base::BindOnce(&OnVerifyAppsEnabledDone, callback_id, j_result));
}

//
// SafeBrowsingApiHandlerBridge
//
SafeBrowsingApiHandlerBridge::SafeBrowsingApiHandlerBridge() {}
SafeBrowsingApiHandlerBridge::~SafeBrowsingApiHandlerBridge() {}

void SafeBrowsingApiHandlerBridge::ClearArtificialDatabase() {
  artificially_marked_phishing_urls_.clear();
}

void SafeBrowsingApiHandlerBridge::PopulateArtificialDatabase() {
  const std::string raw_artificial_urls =
      base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
          switches::kMarkAsPhishing);
  base::StringTokenizer tokenizer(raw_artificial_urls, ",");
  while (tokenizer.GetNext()) {
    auto candidate_url = GURL(tokenizer.token_piece());
    if (candidate_url.is_valid()) {
      artificially_marked_phishing_urls_.insert(candidate_url);
    }
  }
}

void SafeBrowsingApiHandlerBridge::StartHashDatabaseUrlCheck(
    ResponseCallback callback,
    const GURL& url,
    const SBThreatTypeSet& threat_types) {
  bool for_browse_url = SBThreatTypeSetIsValidForCheckBrowseUrl(threat_types);
  if (for_browse_url &&
      base::Contains(threat_types, SBThreatType::SB_THREAT_TYPE_URL_PHISHING) &&
      base::Contains(artificially_marked_phishing_urls_, url)) {
    content::GetUIThreadTaskRunner({})->PostTask(
        FROM_HERE, base::BindOnce(std::move(callback),
                                  SBThreatType::SB_THREAT_TYPE_URL_PHISHING,
                                  ThreatMetadata()));
    return;
  }
  StartUrlCheckBySafeBrowsing(std::move(callback), url, threat_types,
                              SafeBrowsingJavaProtocol::LOCAL_BLOCK_LIST);
}

void SafeBrowsingApiHandlerBridge::StartHashRealTimeUrlCheck(
    ResponseCallback callback,
    const GURL& url,
    const SBThreatTypeSet& threat_types) {
  StartUrlCheckBySafeBrowsing(std::move(callback), url, threat_types,
                              SafeBrowsingJavaProtocol::REAL_TIME);
}

void SafeBrowsingApiHandlerBridge::StartUrlCheckBySafetyNet(
    ResponseCallback callback,
    const GURL& url,
    const SBThreatTypeSet& threat_types) {
  if (interceptor_for_testing_) {
    // For testing, only check the interceptor.
    interceptor_for_testing_->CheckBySafetyNet(std::move(callback), url);
    return;
  }
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  JNIEnv* env = AttachCurrentThread();
  if (!Java_SafeBrowsingApiBridge_ensureSafetyNetApiInitialized(env)) {
    // Mark all requests as safe. Only users who have an old, broken GMSCore or
    // have sideloaded Chrome w/o PlayStore should land here.
    content::GetUIThreadTaskRunner({})->PostTask(
        FROM_HERE,
        base::BindOnce(std::move(callback), SBThreatType::SB_THREAT_TYPE_SAFE,
                       ThreatMetadata()));
    ReportUmaResult(UmaRemoteCallResult::UNSUPPORTED);
    return;
  }

  jlong callback_id = next_safety_net_callback_id_++;
  GetPendingSafetyNetCallbacksMap().insert({callback_id, std::move(callback)});

  DCHECK(!threat_types.empty());

  ScopedJavaLocalRef<jstring> j_url = ConvertUTF8ToJavaString(env, url.spec());
  ScopedJavaLocalRef<jintArray> j_threat_types =
      SBThreatTypeSetToSafetyNetJavaArray(env, threat_types);

  Java_SafeBrowsingApiBridge_startUriLookupBySafetyNetApi(
      env, callback_id, j_url, j_threat_types);
}

void SafeBrowsingApiHandlerBridge::StartUrlCheckBySafeBrowsing(
    ResponseCallback callback,
    const GURL& url,
    const SBThreatTypeSet& threat_types,
    const SafeBrowsingJavaProtocol& protocol) {
  if (interceptor_for_testing_) {
    // For testing, only check the interceptor.
    interceptor_for_testing_->CheckBySafeBrowsing(std::move(callback), url);
    return;
  }
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  base::UmaHistogramBoolean("SafeBrowsing.GmsSafeBrowsingApi.IsAvailable",
                            is_safe_browsing_api_available_);
  base::UmaHistogramBoolean("SafeBrowsing.GmsSafeBrowsingApi.IsAvailable" +
                                GetSafeBrowsingJavaProtocolUmaSuffix(protocol),
                            is_safe_browsing_api_available_);

  if (!is_safe_browsing_api_available_) {
    // Mark all requests as safe. Only users who have an old, broken GMSCore or
    // have sideloaded Chrome w/o PlayStore should land here.
    content::GetUIThreadTaskRunner({})->PostTask(
        FROM_HERE,
        base::BindOnce(std::move(callback), SBThreatType::SB_THREAT_TYPE_SAFE,
                       ThreatMetadata()));
    return;
  }

  JNIEnv* env = AttachCurrentThread();
  jlong callback_id = next_safe_browsing_callback_id_++;
  auto safe_browsing_callback =
      SafeBrowsingResponseCallback(protocol, std::move(callback));
  GetPendingSafeBrowsingCallbacksMap().insert(
      {callback_id, std::move(safe_browsing_callback)});

  DCHECK(!threat_types.empty());

  ScopedJavaLocalRef<jstring> j_url = ConvertUTF8ToJavaString(env, url.spec());
  ScopedJavaLocalRef<jintArray> j_threat_types =
      SBThreatTypeSetToSafeBrowsingJavaArray(env, threat_types);
  int j_int_protocol = static_cast<int>(protocol);

  Java_SafeBrowsingApiBridge_startUriLookupBySafeBrowsingApi(
      env, callback_id, j_url, j_threat_types, j_int_protocol);
}

bool SafeBrowsingApiHandlerBridge::StartCSDAllowlistCheck(const GURL& url) {
  if (interceptor_for_testing_)
    return false;
  return StartAllowlistCheck(
      url, safe_browsing::SBThreatType::SB_THREAT_TYPE_CSD_ALLOWLIST);
}

void SafeBrowsingApiHandlerBridge::StartIsVerifyAppsEnabled(
    VerifyAppsResponseCallback callback) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  if (verify_apps_enabled_for_testing_.has_value()) {
    std::move(callback).Run(verify_apps_enabled_for_testing_.value());
    return;
  }

  JNIEnv* env = AttachCurrentThread();
  if (!Java_SafeBrowsingApiBridge_ensureSafetyNetApiInitialized(env)) {
    std::move(callback).Run(VerifyAppsEnabledResult::FAILED);
    return;
  }

  jlong callback_id = next_verify_apps_callback_id_++;
  GetPendingVerifyAppsCallbacks().insert({callback_id, std::move(callback)});
  Java_SafeBrowsingApiBridge_isVerifyAppsEnabled(env, callback_id);
}
void SafeBrowsingApiHandlerBridge::StartEnableVerifyApps(
    VerifyAppsResponseCallback callback) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  JNIEnv* env = AttachCurrentThread();
  if (!Java_SafeBrowsingApiBridge_ensureSafetyNetApiInitialized(env)) {
    std::move(callback).Run(VerifyAppsEnabledResult::FAILED);
    return;
  }

  jlong callback_id = next_verify_apps_callback_id_++;
  GetPendingVerifyAppsCallbacks().insert({callback_id, std::move(callback)});
  Java_SafeBrowsingApiBridge_enableVerifyApps(env, callback_id);
}

void SafeBrowsingApiHandlerBridge::OnSafeBrowsingApiNonRecoverableFailure() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  is_safe_browsing_api_available_ = false;
}

}  // namespace safe_browsing