chromium/components/translate/content/android/translate_message.cc

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

#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif

#include "components/translate/content/android/translate_message.h"

#include <stddef.h>
#include <stdint.h>

#include <type_traits>
#include <utility>
#include <vector>

#include "base/android/jni_android.h"
#include "base/android/jni_array.h"
#include "base/android/jni_string.h"
#include "base/containers/contains.h"
#include "base/debug/dump_without_crashing.h"
#include "base/logging.h"
#include "base/metrics/field_trial_params.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/utf_string_conversions.h"
#include "components/messages/android/message_enums.h"
#include "components/strings/grit/components_strings.h"
#include "components/translate/core/browser/language_state.h"
#include "components/translate/core/browser/translate_download_manager.h"
#include "components/translate/core/browser/translate_metrics_logger.h"
#include "components/translate/core/browser/translate_ui_delegate.h"
#include "components/translate/core/browser/translate_ui_languages_manager.h"
#include "components/translate/core/common/translate_constants.h"
#include "components/translate/core/common/translate_metrics.h"
#include "content/public/browser/web_contents.h"
#include "ui/base/l10n/l10n_util.h"

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

namespace translate {

namespace {

// Default implementation of the TranslateMessage::Bridge interface, which just
// calls the appropriate Java methods in each case.
class BridgeImpl : public TranslateMessage::Bridge {
 public:
  ~BridgeImpl() override;

  bool CreateTranslateMessage(JNIEnv* env,
                              content::WebContents* web_contents,
                              TranslateMessage* native_translate_message,
                              jint dismissal_duration_seconds) override {
    DCHECK(!java_translate_message_);
    java_translate_message_ = Java_TranslateMessage_create(
        env, web_contents->GetJavaWebContents(),
        reinterpret_cast<intptr_t>(native_translate_message),
        dismissal_duration_seconds);
    return !(!java_translate_message_);
  }

  void ShowTranslateError(JNIEnv* env,
                          content::WebContents* web_contents) override {
    Java_TranslateMessage_showTranslateError(
        env, web_contents->GetJavaWebContents());
  }

  void ShowMessage(
      JNIEnv* env,
      base::android::ScopedJavaLocalRef<jstring> title,
      base::android::ScopedJavaLocalRef<jstring> description,
      base::android::ScopedJavaLocalRef<jstring> primary_button_text,
      jboolean has_overflow_menu) override {
    Java_TranslateMessage_showMessage(
        env, java_translate_message_, std::move(title), std::move(description),
        std::move(primary_button_text), has_overflow_menu);
  }

  base::android::ScopedJavaLocalRef<jobjectArray> ConstructMenuItemArray(
      JNIEnv* env,
      base::android::ScopedJavaLocalRef<jobjectArray> titles,
      base::android::ScopedJavaLocalRef<jobjectArray> subtitles,
      base::android::ScopedJavaLocalRef<jbooleanArray> has_checkmarks,
      base::android::ScopedJavaLocalRef<jintArray> overflow_menu_item_ids,
      base::android::ScopedJavaLocalRef<jobjectArray> language_codes) override {
    return Java_TranslateMessage_constructMenuItemArray(
        env, std::move(titles), std::move(subtitles), std::move(has_checkmarks),
        std::move(overflow_menu_item_ids), std::move(language_codes));
  }

  void ClearNativePointer(JNIEnv* env) override {
    if (java_translate_message_)
      Java_TranslateMessage_clearNativePointer(env, java_translate_message_);
    java_translate_message_ = nullptr;
  }

  void Dismiss(JNIEnv* env) override {
    if (java_translate_message_)
      Java_TranslateMessage_dismiss(env, java_translate_message_);
  }

 private:
  base::android::ScopedJavaGlobalRef<jobject> java_translate_message_;
};

BridgeImpl::~BridgeImpl() = default;

// Returns the auto-dismiss timer duration in seconds for the translate message,
// which defaults to 10s and can be overridden by a Feature param.
int GetDismissalDurationSeconds() {
  constexpr base::FeatureParam<int> kDismissalDuration(
      &kTranslateMessageUI, "dismissal_duration_sec", 10);
  return kDismissalDuration.Get();
}

base::android::ScopedJavaLocalRef<jstring> GetDefaultMessageDescription(
    JNIEnv* env,
    const std::u16string& source_language_display_name,
    const std::u16string& target_language_display_name) {
  return base::android::ConvertUTF16ToJavaString(
      env, l10n_util::GetStringFUTF16(IDS_TRANSLATE_MESSAGE_DESCRIPTION,
                                      source_language_display_name,
                                      target_language_display_name));
}

}  // namespace

// Features
BASE_FEATURE(kTranslateMessageUI,
             "TranslateMessageUI",
             base::FEATURE_ENABLED_BY_DEFAULT);
// Params
const char kTranslateMessageUISnackbarParam[] = "use_snackbar";

TranslateMessage::Bridge::~Bridge() = default;

TranslateMessage::TranslateMessage(
    content::WebContents* web_contents,
    const base::WeakPtr<TranslateManager>& translate_manager,
    base::RepeatingCallback<void()> on_dismiss_callback,
    std::unique_ptr<Bridge> bridge)
    : web_contents_(web_contents),
      translate_manager_(translate_manager),
      on_dismiss_callback_(std::move(on_dismiss_callback)),
      bridge_(std::move(bridge)) {
  DCHECK(!on_dismiss_callback_.is_null());
}

TranslateMessage::TranslateMessage(
    content::WebContents* web_contents,
    const base::WeakPtr<TranslateManager>& translate_manager,
    base::RepeatingCallback<void()> on_dismiss_callback)
    : TranslateMessage(web_contents,
                       translate_manager,
                       std::move(on_dismiss_callback),
                       std::make_unique<BridgeImpl>()) {}

TranslateMessage::~TranslateMessage() {
  // Clear the |on_dismiss_callback_| so that it doesn't get run during object
  // destruction. This prevents a possible use-after-free if the callback points
  // to a method on the owner of |this|.
  on_dismiss_callback_.Reset();

  JNIEnv* env = base::android::AttachCurrentThread();
  if (state_ != State::kDismissed)
    bridge_->Dismiss(env);
}

void TranslateMessage::ShowTranslateStep(TranslateStep step,
                                         const std::string& source_language,
                                         const std::string& target_language) {
  DCHECK(!on_dismiss_callback_.is_null());
  JNIEnv* env = base::android::AttachCurrentThread();

  if (!ui_delegate_) {
    ui_delegate_ = std::make_unique<TranslateUIDelegate>(
        translate_manager_, source_language, target_language);
    ui_languages_manager_ = ui_delegate_->translate_ui_languages_manager();
  }

  if (state_ == State::kDismissed) {
    if (!bridge_->CreateTranslateMessage(env, web_contents_, this,
                                         GetDismissalDurationSeconds())) {
      // The |bridge_| failed to create the Java TranslateMessage, such as when
      // the activity is being destroyed, so there is no message to show.
      return;
    }

    ReportCompactInfobarEvent(InfobarEvent::INFOBAR_IMPRESSION);
  }

  ui_delegate_->UpdateAndRecordSourceLanguage(source_language);
  ui_delegate_->UpdateAndRecordTargetLanguage(target_language);

  if (step == TRANSLATE_STEP_TRANSLATE_ERROR) {
    // Prevent auto-always-translate from triggering if an error occurs.
    is_translation_eligible_for_auto_always_translate_ = false;

    // Count an error as an interaction so that the translation ignored/denied
    // counts don't rise in response to errors. For example, suppose a user
    // wants to translate a webpage that's in Hindi, and has "Always translate
    // pages in Hindi" turned on, but for some reason repeatedly gets
    // translation errors and repeatedly swipes away the message and refreshes
    // the page trying to make translation work. If these are counted as
    // translations being denied, then this could affect decisions made by the
    // translate ranker or cause auto-never-translate-language to trigger
    // inadvertently.
    has_been_interacted_with_ = true;

    bridge_->ShowTranslateError(env, web_contents_);

    // Since an error occurred, show the UI in the last good state.
    const LanguageState* language_state = ui_delegate_->GetLanguageState();
    if (language_state && language_state->IsPageTranslated())
      step = TRANSLATE_STEP_AFTER_TRANSLATE;
    else
      step = TRANSLATE_STEP_BEFORE_TRANSLATE;
  }

  const std::u16string& source_language_display_name =
      ui_languages_manager_->GetLanguageNameAt(
          ui_languages_manager_->GetSourceLanguageIndex());
  const std::u16string& target_language_display_name =
      ui_languages_manager_->GetLanguageNameAt(
          ui_languages_manager_->GetTargetLanguageIndex());

  base::android::ScopedJavaLocalRef<jstring> title;
  base::android::ScopedJavaLocalRef<jstring> description;
  base::android::ScopedJavaLocalRef<jstring> primary_button_text;

  switch (step) {
    case TRANSLATE_STEP_BEFORE_TRANSLATE:
      title = base::android::ConvertUTF16ToJavaString(
          env, l10n_util::GetStringUTF16(
                   IDS_TRANSLATE_MESSAGE_BEFORE_TRANSLATE_TITLE));
      description = GetDefaultMessageDescription(
          env, source_language_display_name, target_language_display_name);
      primary_button_text = base::android::ConvertUTF16ToJavaString(
          env, l10n_util::GetStringUTF16(IDS_TRANSLATE_BUTTON));

      state_ = State::kBeforeTranslate;
      is_translation_eligible_for_auto_always_translate_ = false;
      break;

    case TRANSLATE_STEP_TRANSLATING:
      if (state_ == State::kDismissed) {
        // If the UI is currently not shown and being opened directly into the
        // translation-in-progress state (e.g. if the page was loaded and
        // "Always translate pages in <language>" triggered, or if the
        // "Translate" menu item was clicked in the browser 3-dots menu), show a
        // separate title string indicating that the translation is in progress
        // instead of the default "Translate page?" title used in the
        // before-translate state.
        title = base::android::ConvertUTF16ToJavaString(
            env, l10n_util::GetStringUTF16(
                     IDS_TRANSLATE_MESSAGE_TRANSLATING_COLD_OPEN_TITLE));
      } else {
        title = base::android::ConvertUTF16ToJavaString(
            env, l10n_util::GetStringUTF16(
                     IDS_TRANSLATE_MESSAGE_BEFORE_TRANSLATE_TITLE));
      }
      description = GetDefaultMessageDescription(
          env, source_language_display_name, target_language_display_name);
      primary_button_text = nullptr;

      state_ = State::kTranslating;
      break;

    case TRANSLATE_STEP_AFTER_TRANSLATE:
      title = base::android::ConvertUTF16ToJavaString(
          env, l10n_util::GetStringUTF16(
                   IDS_TRANSLATE_MESSAGE_AFTER_TRANSLATE_TITLE));
      primary_button_text = base::android::ConvertUTF16ToJavaString(
          env, l10n_util::GetStringUTF16(IDS_TRANSLATE_MESSAGE_UNDO_BUTTON));

      if (is_translation_eligible_for_auto_always_translate_ &&
          ui_delegate_->ShouldAutoAlwaysTranslate()) {
        ReportCompactInfobarEvent(
            InfobarEvent::INFOBAR_SNACKBAR_AUTO_ALWAYS_IMPRESSION);
        ui_delegate_->SetAlwaysTranslate(true);

        description = base::android::ConvertUTF16ToJavaString(
            env,
            l10n_util::GetStringFUTF16(
                IDS_TRANSLATE_MESSAGE_AUTO_ALWAYS_TRANSLATE_LANGUAGE_DESCRIPTION,
                source_language_display_name, target_language_display_name));

        state_ = State::kAfterTranslateWithAutoAlwaysConfirmation;
      } else {
        description = GetDefaultMessageDescription(
            env, source_language_display_name, target_language_display_name);
        state_ = State::kAfterTranslate;
      }

      is_translation_eligible_for_auto_always_translate_ = false;
      break;

    default:
      NOTREACHED_IN_MIGRATION();
      break;
  }

  bridge_->ShowMessage(env, std::move(title), std::move(description),
                       std::move(primary_button_text),
                       /*has_overflow_menu=*/true);
}

void TranslateMessage::HandlePrimaryAction(JNIEnv* env) {
  has_been_interacted_with_ = true;
  is_translation_eligible_for_auto_always_translate_ = false;

  switch (state_) {
    case State::kBeforeTranslate:
      ReportCompactInfobarEvent(InfobarEvent::INFOBAR_TARGET_TAB_TRANSLATE);
      is_translation_eligible_for_auto_always_translate_ = true;
      ui_delegate_->ReportUIInteraction(UIInteraction::kTranslate);
      ui_delegate_->Translate();
      break;

    case State::kTranslating:
      // Should not happen, but per https://crbug.com/1409304 it may, so add
      // logging.
      base::debug::DumpWithoutCrashing();
      break;
    case State::kAfterTranslateWithAutoAlwaysConfirmation:
      // The user clicked "Undo" on a translated page when the
      // auto-always-translate confirmation message was showing, so turn off
      // "always translate language" before reverting the translation.
      ReportCompactInfobarEvent(
          InfobarEvent::INFOBAR_SNACKBAR_CANCEL_AUTO_ALWAYS);
      ui_delegate_->SetAlwaysTranslate(false);
      [[fallthrough]];
    case State::kAfterTranslate:
      ReportCompactInfobarEvent(InfobarEvent::INFOBAR_REVERT);
      ui_delegate_->ReportUIInteraction(UIInteraction::kRevert);
      RevertTranslationAndUpdateMessage();
      break;

    case State::kAutoNeverTranslateConfirmation:
      // The user clicked "Undo" on the message confirming that pages in this
      // language will not be translated, so unblock that language. Also, since
      // this confirmation message is only shown after the user has already
      // tried to dismiss the translate UI, dismiss this popup as well.
      ReportCompactInfobarEvent(
          InfobarEvent::INFOBAR_SNACKBAR_CANCEL_AUTO_NEVER);
      ui_delegate_->SetLanguageBlocked(false);
      bridge_->Dismiss(env);
      break;
    case State::kDismissed:
      // Should not happen, but per https://crbug.com/1409304 it may, so add
      // logging.
      base::debug::DumpWithoutCrashing();
      break;
    default:
      NOTREACHED_IN_MIGRATION();
      break;
  }
}

void TranslateMessage::HandleDismiss(JNIEnv* env, jint dismiss_reason) {
  switch (static_cast<messages::DismissReason>(dismiss_reason)) {
    case messages::DismissReason::GESTURE:
      ui_delegate_->OnUIClosedByUser();
      ui_delegate_->ReportUIInteraction(UIInteraction::kCloseUIExplicitly);
      break;

    case messages::DismissReason::TAB_SWITCHED:
    case messages::DismissReason::TAB_DESTROYED:
    case messages::DismissReason::ACTIVITY_DESTROYED:
    case messages::DismissReason::SCOPE_DESTROYED:
      ui_delegate_->ReportUIInteraction(UIInteraction::kCloseUILostFocus);
      break;

    case messages::DismissReason::TIMER:
      ui_delegate_->ReportUIInteraction(UIInteraction::kCloseUITimerRanOut);
      break;

    case messages::DismissReason::PRIMARY_ACTION:
    case messages::DismissReason::SECONDARY_ACTION:
      // These dismiss reasons should not be possible for a TranslateMessage,
      // since clicking the primary or secondary buttons doesn't dismiss the
      // message.
      NOTREACHED_IN_MIGRATION();
      break;

    default:
      break;
  }

  if (!has_been_interacted_with_ && state_ == State::kBeforeTranslate) {
    ReportCompactInfobarEvent(InfobarEvent::INFOBAR_DECLINE);

    // In order to have the same off-by-one counting as the infobar UI,
    // ShouldAutoNeverTranslate() must be called before TranslationDeclined().
    const bool should_auto_never_translate =
        static_cast<messages::DismissReason>(dismiss_reason) ==
            messages::DismissReason::GESTURE &&
        ui_delegate_->ShouldAutoNeverTranslate();

    ui_delegate_->TranslationDeclined(
        static_cast<messages::DismissReason>(dismiss_reason) ==
        messages::DismissReason::GESTURE);

    if (should_auto_never_translate) {
      ReportCompactInfobarEvent(
          InfobarEvent::INFOBAR_SNACKBAR_AUTO_NEVER_IMPRESSION);

      ui_delegate_->SetLanguageBlocked(true);
      state_ = State::kAutoNeverTranslateConfirmation;

      bridge_->ShowMessage(
          env, /*title=*/
          base::android::ConvertUTF16ToJavaString(
              env,
              l10n_util::GetStringFUTF16(
                  IDS_TRANSLATE_MESSAGE_AUTO_NEVER_TRANSLATE_LANGUAGE_TITLE,
                  ui_languages_manager_->GetLanguageNameAt(
                      ui_languages_manager_->GetSourceLanguageIndex()))),
          /*description=*/nullptr,
          /*primary_button_text=*/
          base::android::ConvertUTF16ToJavaString(
              env, l10n_util::GetStringUTF16(IDS_TRANSLATE_NOTIFICATION_UNDO)),
          /*has_overflow_menu=*/false);

      // Return early here without calling the dismiss callback, since the
      // dismiss callback could try to do things like show an IPH tooltip, which
      // would be obscured by the auto-never-translate confirmation message.
      return;
    }
  }

  bridge_->ClearNativePointer(env);
  state_ = State::kDismissed;

  // The only time |on_dismiss_callback_| will be null is during the destruction
  // of |this|.
  if (!on_dismiss_callback_.is_null()) {
    // Note that this callback can destroy |this|, so this method shouldn't do
    // anything afterwards.
    on_dismiss_callback_.Run();
  }
}

base::android::ScopedJavaLocalRef<jobjectArray>
TranslateMessage::BuildOverflowMenu(JNIEnv* env) {
  ReportCompactInfobarEvent(InfobarEvent::INFOBAR_OPTIONS);

  has_been_interacted_with_ = true;

  // If the overflow menu is open when auto-always-translate triggers, then the
  // "Always translate language" option in the menu would remain unchecked even
  // if auto-always-translate triggers and changes that setting. To avoid this
  // inconsistency, don't try to turn on auto-always-translate if the user
  // opened the overflow menu mid-translation.
  is_translation_eligible_for_auto_always_translate_ = false;

  // |titles| must have the capacity to fit the maximum number of menu items in
  // the overflow menu, including dividers.
  std::u16string titles[1U +  // Change target language.
                        1U +  // Divider.
                        1U +  // Always translate language.
                        1U +  // Never translate language.
                        1U +  // Never translate site.
                        1U];  // Change source language.

  // |has_checkmarks| is value initialized full of |false|.
  bool has_checkmarks[std::extent<decltype(titles)>::value] = {};
  int overflow_menu_item_ids[std::extent<decltype(titles)>::value] = {};

  size_t item_count = 0U;

  const std::u16string& source_language_display_name =
      ui_languages_manager_->GetLanguageNameAt(
          ui_languages_manager_->GetSourceLanguageIndex());

  // "More languages".
  CHECK_GT(std::extent<decltype(titles)>::value, item_count);
  titles[item_count] =
      l10n_util::GetStringUTF16(IDS_TRANSLATE_OPTION_MORE_LANGUAGE);
  overflow_menu_item_ids[item_count++] =
      static_cast<int>(OverflowMenuItemId::kChangeTargetLanguage);

  // Menu item divider.
  CHECK_GT(std::extent<decltype(titles)>::value, item_count);
  overflow_menu_item_ids[item_count++] =
      static_cast<int>(OverflowMenuItemId::kInvalid);

  if (!ui_delegate_->IsIncognito() &&
      ui_languages_manager_->GetSourceLanguageCode() != kUnknownLanguageCode) {
    // "Always translate pages in <source language>".
    CHECK_GT(std::extent<decltype(titles)>::value, item_count);
    titles[item_count] = l10n_util::GetStringFUTF16(
        IDS_TRANSLATE_MESSAGE_ALWAYS_TRANSLATE_LANGUAGE,
        source_language_display_name);
    has_checkmarks[item_count] = ui_delegate_->ShouldAlwaysTranslate();
    overflow_menu_item_ids[item_count++] =
        static_cast<int>(OverflowMenuItemId::kToggleAlwaysTranslateLanguage);
  }

  if (ui_languages_manager_->GetSourceLanguageCode() != kUnknownLanguageCode) {
    // "Never translate pages in <source language>".
    CHECK_GT(std::extent<decltype(titles)>::value, item_count);
    titles[item_count] = l10n_util::GetStringFUTF16(
        IDS_TRANSLATE_MESSAGE_NEVER_TRANSLATE_LANGUAGE,
        source_language_display_name);
    has_checkmarks[item_count] = ui_delegate_->IsLanguageBlocked();
    overflow_menu_item_ids[item_count++] =
        static_cast<int>(OverflowMenuItemId::kToggleNeverTranslateLanguage);
  }

  if (ui_delegate_->CanAddSiteToNeverPromptList()) {
    // "Never translate this site".
    CHECK_GT(std::extent<decltype(titles)>::value, item_count);
    titles[item_count] =
        l10n_util::GetStringUTF16(IDS_TRANSLATE_NEVER_TRANSLATE_SITE);
    has_checkmarks[item_count] = ui_delegate_->IsSiteOnNeverPromptList();
    overflow_menu_item_ids[item_count++] =
        static_cast<int>(OverflowMenuItemId::kToggleNeverTranslateSite);
  }

  // "Page is not in <source language>?".
  CHECK_GT(std::extent<decltype(titles)>::value, item_count);
  titles[item_count] = l10n_util::GetStringFUTF16(
      IDS_TRANSLATE_INFOBAR_OPTIONS_NOT_SOURCE_LANGUAGE,
      source_language_display_name);
  overflow_menu_item_ids[item_count++] =
      static_cast<int>(OverflowMenuItemId::kChangeSourceLanguage);

  // Pass arrays of empty strings for both |subtitles| and |language_codes|.
  std::u16string subtitles[std::extent<decltype(titles)>::value];
  std::string language_codes[std::extent<decltype(titles)>::value];

  return bridge_->ConstructMenuItemArray(
      env,
      base::android::ToJavaArrayOfStrings(env,
                                          base::make_span(titles, item_count)),
      base::android::ToJavaArrayOfStrings(
          env, base::make_span(subtitles, item_count)),
      base::android::ToJavaBooleanArray(env, has_checkmarks, item_count),
      base::android::ToJavaIntArray(env, overflow_menu_item_ids, item_count),
      base::android::ToJavaArrayOfStrings(
          env, base::make_span(language_codes, item_count)));
}

base::android::ScopedJavaLocalRef<jobjectArray>
TranslateMessage::HandleSecondaryMenuItemClicked(
    JNIEnv* env,
    jint overflow_menu_item_id,
    const base::android::JavaRef<jstring>& language_code,
    jboolean had_checkmark) {
  has_been_interacted_with_ = true;

  // Interacting with the secondary menu can cause the page to be translated or
  // an existing translation to be reverted, so to avoid these edge cases, don't
  // try to turn on auto-always-translate.
  is_translation_eligible_for_auto_always_translate_ = false;

  std::string language_code_utf8 =
      base::android::ConvertJavaStringToUTF8(env, language_code);
  if (!language_code_utf8.empty()) {
    switch (static_cast<OverflowMenuItemId>(overflow_menu_item_id)) {
      case OverflowMenuItemId::kChangeSourceLanguage:
        ui_delegate_->ReportUIInteraction(UIInteraction::kChangeSourceLanguage);
        ui_delegate_->UpdateAndRecordSourceLanguage(language_code_utf8);
        ui_delegate_->Translate();
        break;

      case OverflowMenuItemId::kChangeTargetLanguage:
        ReportCompactInfobarEvent(
            InfobarEvent::INFOBAR_MORE_LANGUAGES_TRANSLATE);
        ui_delegate_->ReportUIInteraction(UIInteraction::kChangeTargetLanguage);
        ui_delegate_->UpdateAndRecordTargetLanguage(language_code_utf8);
        ui_delegate_->Translate();
        break;

      default:
        NOTREACHED_IN_MIGRATION();
        break;
    }
    return nullptr;
  }

  const bool desired_toggle_value = !had_checkmark;

  switch (static_cast<OverflowMenuItemId>(overflow_menu_item_id)) {
    case OverflowMenuItemId::kChangeSourceLanguage: {
      ReportCompactInfobarEvent(InfobarEvent::INFOBAR_PAGE_NOT_IN);
      const std::string skip_language_codes[] = {
          ui_languages_manager_->GetSourceLanguageCode()};
      return ConstructLanguagePickerMenu(
          env, OverflowMenuItemId::kChangeSourceLanguage,
          /*content_language_codes=*/base::span<const std::string>(),
          skip_language_codes);
    }

    case OverflowMenuItemId::kChangeTargetLanguage: {
      ReportCompactInfobarEvent(InfobarEvent::INFOBAR_MORE_LANGUAGES);
      const std::string skip_language_codes[] = {
          ui_languages_manager_->GetTargetLanguageCode(), kUnknownLanguageCode};
      std::vector<std::string> content_language_codes;
      ui_delegate_->GetContentLanguagesCodes(&content_language_codes);
      return ConstructLanguagePickerMenu(
          env, OverflowMenuItemId::kChangeTargetLanguage,
          content_language_codes, skip_language_codes);
    }

    case OverflowMenuItemId::kToggleAlwaysTranslateLanguage:
      if (ui_delegate_->ShouldAlwaysTranslate() != desired_toggle_value) {
        ReportCompactInfobarEvent(
            desired_toggle_value ? InfobarEvent::INFOBAR_ALWAYS_TRANSLATE
                                 : InfobarEvent::INFOBAR_ALWAYS_TRANSLATE_UNDO);
        ui_delegate_->SetAlwaysTranslate(desired_toggle_value);
      }

      if (desired_toggle_value && state_ == State::kBeforeTranslate)
        ui_delegate_->Translate();
      break;

    case OverflowMenuItemId::kToggleNeverTranslateLanguage:
      if (ui_delegate_->IsLanguageBlocked() != desired_toggle_value) {
        ReportCompactInfobarEvent(
            desired_toggle_value ? InfobarEvent::INFOBAR_NEVER_TRANSLATE
                                 : InfobarEvent::INFOBAR_NEVER_TRANSLATE_UNDO);
        ui_delegate_->SetLanguageBlocked(desired_toggle_value);
      }

      if (desired_toggle_value &&
          (state_ == State::kTranslating || state_ == State::kAfterTranslate ||
           state_ == State::kAfterTranslateWithAutoAlwaysConfirmation)) {
        RevertTranslationAndUpdateMessage();
      }
      break;

    case OverflowMenuItemId::kToggleNeverTranslateSite:
      if (ui_delegate_->IsSiteOnNeverPromptList() != desired_toggle_value) {
        ReportCompactInfobarEvent(
            desired_toggle_value
                ? InfobarEvent::INFOBAR_NEVER_TRANSLATE_SITE
                : InfobarEvent::INFOBAR_NEVER_TRANSLATE_SITE_UNDO);
        ui_delegate_->SetNeverPromptSite(desired_toggle_value);
      }

      if (desired_toggle_value &&
          (state_ == State::kTranslating || state_ == State::kAfterTranslate ||
           state_ == State::kAfterTranslateWithAutoAlwaysConfirmation)) {
        RevertTranslationAndUpdateMessage();
      }
      break;

    default:
      NOTREACHED_IN_MIGRATION();
      break;
  }

  return nullptr;
}

void TranslateMessage::RevertTranslationAndUpdateMessage() {
  // The TranslateManager doesn't make another call to show the pre-translation
  // UI during RevertTranslation(), so show the correct UI here. This is done
  // before calling RevertTranslation() just in case RevertTranslation() causes
  // this message to be dismissed or destroyed, since that could cause a
  // use-after-free.
  ShowTranslateStep(TRANSLATE_STEP_BEFORE_TRANSLATE,
                    ui_languages_manager_->GetSourceLanguageCode(),
                    ui_languages_manager_->GetTargetLanguageCode());

  ui_delegate_->RevertTranslation();
}

base::android::ScopedJavaLocalRef<jobjectArray>
TranslateMessage::ConstructLanguagePickerMenu(
    JNIEnv* env,
    OverflowMenuItemId overflow_menu_item_id,
    base::span<const std::string> content_language_codes,
    base::span<const std::string> skip_language_codes) const {
  std::vector<std::u16string> titles;
  std::vector<std::u16string> subtitles;
  std::vector<int> overflow_menu_item_ids;
  std::vector<std::string> language_codes;

  // Add the content languages to the menu.
  for (const std::string& content_language_code : content_language_codes) {
    if (base::Contains(skip_language_codes, content_language_code)) {
      continue;
    }

    titles.emplace_back(l10n_util::GetDisplayNameForLocale(
        content_language_code,
        TranslateDownloadManager::GetInstance()->application_locale(),
        /*is_for_ui=*/true));
    subtitles.emplace_back(l10n_util::GetDisplayNameForLocale(
        content_language_code, content_language_code, /*is_for_ui=*/true));
    overflow_menu_item_ids.emplace_back(
        static_cast<int>(overflow_menu_item_id));
    language_codes.emplace_back(content_language_code);
  }

  if (!titles.empty()) {
    // Add a divider between the content languages and the later full list of
    // languages.
    titles.emplace_back(std::u16string());
    subtitles.emplace_back(std::u16string());
    overflow_menu_item_ids.emplace_back(
        static_cast<int>(OverflowMenuItemId::kInvalid));
    language_codes.emplace_back(std::string());
  }

  // Add the full list of languages to the menu.
  for (size_t i = 0U; i < ui_languages_manager_->GetNumberOfLanguages(); ++i) {
    std::string code = ui_languages_manager_->GetLanguageCodeAt(i);
    if (base::Contains(skip_language_codes, code)) {
      continue;
    }

    titles.emplace_back(ui_languages_manager_->GetLanguageNameAt(i));
    subtitles.emplace_back();
    overflow_menu_item_ids.emplace_back(
        static_cast<int>(overflow_menu_item_id));
    language_codes.emplace_back(std::move(code));
  }

  return bridge_->ConstructMenuItemArray(
      env, base::android::ToJavaArrayOfStrings(env, titles),
      base::android::ToJavaArrayOfStrings(env, subtitles),
      /*has_checkmarks=*/
      base::android::ToJavaBooleanArray(
          env, std::make_unique<bool[]>(titles.size()).get(), titles.size()),
      base::android::ToJavaIntArray(env, overflow_menu_item_ids),
      base::android::ToJavaArrayOfStrings(env, language_codes));
}

}  // namespace translate