chromium/ui/base/ime/win/tsf_bridge.cc

// Copyright 2018 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 "ui/base/ime/win/tsf_bridge.h"

#include <msctf.h>

#include <map>

#include "base/logging.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/ref_counted.h"
#include "base/no_destructor.h"
#include "base/task/current_thread.h"
#include "base/threading/thread_local.h"
#include "base/trace_event/trace_event.h"
#include "base/win/scoped_variant.h"
#include "ui/base/ime/ime_key_event_dispatcher.h"
#include "ui/base/ime/text_input_client.h"
#include "ui/base/ime/win/mock_tsf_bridge.h"
#include "ui/base/ime/win/tsf_text_store.h"
#include "ui/base/ui_base_features.h"

namespace ui {

namespace {

// TSFBridgeImpl -----------------------------------------------------------

// A TLS implementation of TSFBridge.
class TSFBridgeImpl : public TSFBridge {
 public:
  TSFBridgeImpl();

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

  ~TSFBridgeImpl() override;

  HRESULT Initialize();

  // TsfBridge:
  void OnTextInputTypeChanged(const TextInputClient* client) override;
  void OnTextLayoutChanged() override;
  bool CancelComposition() override;
  bool ConfirmComposition() override;
  void SetFocusedClient(HWND focused_window, TextInputClient* client) override;
  void RemoveFocusedClient(TextInputClient* client) override;
  void SetImeKeyEventDispatcher(
      ImeKeyEventDispatcher* ime_key_event_dispatcher) override;
  void RemoveImeKeyEventDispatcher(
      ImeKeyEventDispatcher* ime_key_event_dispatcher) override;
  bool IsInputLanguageCJK() override;
  Microsoft::WRL::ComPtr<ITfThreadMgr> GetThreadManager() override;
  TextInputClient* GetFocusedTextInputClient() const override;
  void OnUrlChanged() override;

 private:
  // Returns S_OK if |tsf_document_map_| is successfully initialized. This
  // method should be called from and only from Initialize().
  HRESULT InitializeDocumentMapInternal();

  // Returns S_OK if |context| is successfully updated to be a disabled
  // context, where an IME should be deactivated. This is suitable for some
  // special input context such as password fields.
  HRESULT InitializeDisabledContext(ITfContext* context);

  // Returns S_OK if a TSF document manager and a TSF context is successfully
  // created with associating with given |text_store|. The returned
  // |source_cookie| indicates the binding between |text_store| and |context|.
  // You can pass nullptr to |text_store| and |source_cookie| when text store is
  // not necessary.
  HRESULT CreateDocumentManager(TSFTextStore* text_store,
                                ITfDocumentMgr** document_manager,
                                ITfContext** context,
                                DWORD* source_cookie,
                                DWORD* key_trace_sink_cookie,
                                DWORD* language_profile_cookie);

  // Returns true if |document_manager| is the focused document manager.
  bool IsFocused(ITfDocumentMgr* document_manager);

  // Returns true if already initialized.
  bool IsInitialized();

  // Updates or clears the association maintained in the TSF runtime between
  // |attached_window_handle_| and the current document manager. Keeping this
  // association updated solves some tricky event ordering issues between
  // logical text input focus managed by Chrome and native text input focus
  // managed by the OS.
  // Background:
  //   TSF runtime monitors some Win32 messages such as WM_ACTIVATE to
  //   change the focused document manager. This is problematic when
  //   TSFBridge::SetFocusedClient is called first then the target window
  //   receives WM_ACTIVATE. This actually occurs in Aura environment where
  //   WM_NCACTIVATE is used as a trigger to restore text input focus.
  // Caveats:
  //   TSF runtime does not increment the reference count of the attached
  //   document manager. See the comment inside the method body for
  //   details.
  void UpdateAssociateFocus();
  void ClearAssociateFocus();

  // A triple of document manager, text store and binding cookie between
  // a context owned by the document manager and the text store. This is a
  // minimum working set of an editable document in TSF.
  struct TSFDocument {
   public:
    TSFDocument()
        : source_cookie(TF_INVALID_COOKIE),
          key_trace_sink_cookie(TF_INVALID_COOKIE),
          language_profile_cookie(TF_INVALID_COOKIE) {}
    TSFDocument(const TSFDocument& src)
        : document_manager(src.document_manager),
          source_cookie(src.source_cookie),
          key_trace_sink_cookie(src.key_trace_sink_cookie),
          language_profile_cookie(src.language_profile_cookie) {}
    Microsoft::WRL::ComPtr<ITfDocumentMgr> document_manager;
    scoped_refptr<TSFTextStore> text_store;
    DWORD source_cookie;
    DWORD key_trace_sink_cookie;
    DWORD language_profile_cookie;
  };

  // Returns a pointer to TSFDocument that is associated with the current
  // TextInputType of |client_|.
  TSFDocument* GetAssociatedDocument();

  // An ITfThreadMgr object to be used in focus and document management.
  Microsoft::WRL::ComPtr<ITfThreadMgr> thread_manager_;

  // An ITfInputProcessorProfiles object to be used to get current language
  // locale profile.
  Microsoft::WRL::ComPtr<ITfInputProcessorProfiles> input_processor_profiles_;

  // A map from TextInputType to an editable document for TSF. We use multiple
  // TSF documents that have different InputScopes and TSF attributes based on
  // the TextInputType associated with the target document. For a TextInputType
  // that is not coverted by this map, a default document, e.g. the document
  // for TEXT_INPUT_TYPE_TEXT, should be used.
  // Note that some IMEs don't change their state unless the document focus is
  // changed. This is why we use multiple documents instead of changing TSF
  // metadata of a single document on the fly.
  typedef std::map<TextInputType, TSFDocument> TSFDocumentMap;
  TSFDocumentMap tsf_document_map_;

  // An identifier of TSF client.
  TfClientId client_id_ = TF_CLIENTID_NULL;

  // Current focused text input client. Do not free |client_|.
  raw_ptr<TextInputClient> client_ = nullptr;

  // Input Type of current focused text input client.
  TextInputType input_type_ = TEXT_INPUT_TYPE_NONE;

  // Represents the window that is currently owns text input focus.
  HWND attached_window_handle_ = nullptr;

  // Tracks Windows OS support for empty TSF text stores, available on win11+.
  bool empty_tsf_support_ = false;
};

TSFBridgeImpl::TSFBridgeImpl() = default;

TSFBridgeImpl::~TSFBridgeImpl() {
  DCHECK(base::CurrentUIThread::IsSet());
  if (!IsInitialized())
    return;

  for (auto& pair : tsf_document_map_) {
    TSFDocument& document = pair.second;
    Microsoft::WRL::ComPtr<ITfContext> context;
    Microsoft::WRL::ComPtr<ITfSource> source;
    if (document.source_cookie != TF_INVALID_COOKIE &&
        SUCCEEDED(document.document_manager->GetBase(&context)) &&
        SUCCEEDED(context.As(&source))) {
      source->UnadviseSink(document.source_cookie);
    }
    if (thread_manager_ != nullptr) {
      Microsoft::WRL::ComPtr<ITfSource> key_trace_sink_source;
      if (document.key_trace_sink_cookie != TF_INVALID_COOKIE &&
          SUCCEEDED(thread_manager_.As(&key_trace_sink_source))) {
        key_trace_sink_source->UnadviseSink(document.key_trace_sink_cookie);
      }
      Microsoft::WRL::ComPtr<ITfSource> language_profile_source;
      if (document.language_profile_cookie != TF_INVALID_COOKIE &&
          SUCCEEDED(input_processor_profiles_.As(&language_profile_source))) {
        language_profile_source->UnadviseSink(document.language_profile_cookie);
      }
    }
  }
  tsf_document_map_.clear();

  client_id_ = TF_CLIENTID_NULL;
}

HRESULT TSFBridgeImpl::Initialize() {
  DCHECK(base::CurrentUIThread::IsSet());
  if (client_id_ != TF_CLIENTID_NULL) {
    DVLOG(1) << "Already initialized.";
    return S_OK;  // shouldn't return error code in this case.
  }

  HRESULT hr =
      ::CoCreateInstance(CLSID_TF_InputProcessorProfiles, nullptr, CLSCTX_ALL,
                         IID_PPV_ARGS(&input_processor_profiles_));
  if (FAILED(hr)) {
    DVLOG(1) << "Failed to create InputProcessorProfiles instance.";
    return hr;
  }

  hr = ::CoCreateInstance(CLSID_TF_ThreadMgr, nullptr, CLSCTX_ALL,
                          IID_PPV_ARGS(&thread_manager_));
  if (FAILED(hr)) {
    DVLOG(1) << "Failed to create ThreadManager instance.";
    return hr;
  }

  hr = thread_manager_->Activate(&client_id_);
  if (FAILED(hr)) {
    DVLOG(1) << "Failed to activate Thread Manager.";
    return hr;
  }

  hr = InitializeDocumentMapInternal();
  if (FAILED(hr))
    return hr;

  // Japanese IME expects the default value of this compartment is
  // TF_SENTENCEMODE_PHRASEPREDICT like IMM32 implementation. This value is
  // managed per thread, so that it is enough to set this value at once. This
  // value does not affect other language's IME behaviors.
  Microsoft::WRL::ComPtr<ITfCompartmentMgr> thread_compartment_manager;
  hr = thread_manager_.As(&thread_compartment_manager);
  if (FAILED(hr)) {
    DVLOG(1) << "Failed to get ITfCompartmentMgr.";
    return hr;
  }

  Microsoft::WRL::ComPtr<ITfCompartment> sentence_compartment;
  hr = thread_compartment_manager->GetCompartment(
      GUID_COMPARTMENT_KEYBOARD_INPUTMODE_SENTENCE, &sentence_compartment);
  if (FAILED(hr)) {
    DVLOG(1) << "Failed to get sentence compartment.";
    return hr;
  }

  base::win::ScopedVariant sentence_variant;
  sentence_variant.Set(TF_SENTENCEMODE_PHRASEPREDICT);
  hr = sentence_compartment->SetValue(client_id_, sentence_variant.ptr());
  if (FAILED(hr)) {
    DVLOG(1) << "Failed to change the sentence mode.";
    return hr;
  }

  return S_OK;
}

void TSFBridgeImpl::OnTextInputTypeChanged(const TextInputClient* client) {
  DCHECK(base::CurrentUIThread::IsSet());
  DCHECK(IsInitialized());

  if (client != client_) {
    // Called from not focusing client. Do nothing.
    return;
  }

  // Since we reuse TSF document for same text input type, there is a case where
  // focus is switched between two text fields with same input type. We should
  // prepare the TSF document for reuse by clearing focus first.
  if (input_type_ != TEXT_INPUT_TYPE_NONE &&
      input_type_ == client_->GetTextInputType()) {
    if (empty_tsf_support_) {
      // Switch focus to empty doc. This optimizes the reuse, since here TSF
      // changes the text store's edit context state. So switching
      // the focus back to the edit context helps to reuse the TSF document.
      // It also reduces focus noise to TSF's default document which is only
      // there for app compat with embedded controls.
      TSFDocumentMap::iterator it =
          tsf_document_map_.find(TEXT_INPUT_TYPE_NONE);
      if (it != tsf_document_map_.end()) {
        thread_manager_->SetFocus(it->second.document_manager.Get());
      } else {
        // If we don't have an empty document, set focus to TSF default
        // document.
        thread_manager_->SetFocus(nullptr);
      }
    } else {
      thread_manager_->SetFocus(nullptr);
    }
  }
  input_type_ = client_->GetTextInputType();
  TSFDocument* document = GetAssociatedDocument();
  if (!document)
    return;
  // We call AssociateFocus for text input type none that also
  // triggers SetFocus internally. We don't want to send multiple
  // focus notifications for the same text input type so we don't
  // call AssociateFocus and SetFocus together. Just calling SetFocus
  // should be sufficient for setting focus on a textstore.
  if (input_type_ != TEXT_INPUT_TYPE_NONE)
    thread_manager_->SetFocus(document->document_manager.Get());
  else
    UpdateAssociateFocus();
  OnTextLayoutChanged();
}

void TSFBridgeImpl::OnTextLayoutChanged() {
  TSFDocument* document = GetAssociatedDocument();
  if (!document)
    return;
  if (!document->text_store)
    return;
  document->text_store->SendOnLayoutChange();
}

bool TSFBridgeImpl::CancelComposition() {
  DCHECK(base::CurrentUIThread::IsSet());
  DCHECK(IsInitialized());

  TSFDocument* document = GetAssociatedDocument();
  if (!document)
    return false;
  if (!document->text_store)
    return false;

  return document->text_store->CancelComposition();
}

bool TSFBridgeImpl::ConfirmComposition() {
  DCHECK(base::CurrentUIThread::IsSet());
  DCHECK(IsInitialized());

  TSFDocument* document = GetAssociatedDocument();
  if (!document)
    return false;
  if (!document->text_store)
    return false;

  return document->text_store->ConfirmComposition();
}

void TSFBridgeImpl::SetFocusedClient(HWND focused_window,
                                     TextInputClient* client) {
  DCHECK(base::CurrentUIThread::IsSet());
  DCHECK(client);
  DCHECK(IsInitialized());
  if (attached_window_handle_ != focused_window)
    ClearAssociateFocus();
  client_ = client;
  attached_window_handle_ = focused_window;

  for (TSFDocumentMap::iterator it = tsf_document_map_.begin();
       it != tsf_document_map_.end(); ++it) {
    if (it->second.text_store.get() == nullptr)
      continue;
    it->second.text_store->SetFocusedTextInputClient(focused_window, client);
  }

  // Synchronize text input type state.
  OnTextInputTypeChanged(client);
}

void TSFBridgeImpl::RemoveFocusedClient(TextInputClient* client) {
  DCHECK(base::CurrentUIThread::IsSet());
  DCHECK(IsInitialized());
  if (client_ != client)
    return;
  ClearAssociateFocus();
  client_ = nullptr;
  attached_window_handle_ = nullptr;
  for (TSFDocumentMap::iterator it = tsf_document_map_.begin();
       it != tsf_document_map_.end(); ++it) {
    if (it->second.text_store.get() == nullptr)
      continue;
    it->second.text_store->SetFocusedTextInputClient(nullptr, nullptr);
  }
}

void TSFBridgeImpl::SetImeKeyEventDispatcher(
    ImeKeyEventDispatcher* ime_key_event_dispatcher) {
  DCHECK(base::CurrentUIThread::IsSet());
  DCHECK(ime_key_event_dispatcher);
  DCHECK(IsInitialized());

  for (TSFDocumentMap::iterator it = tsf_document_map_.begin();
       it != tsf_document_map_.end(); ++it) {
    if (it->second.text_store.get() == nullptr)
      continue;
    it->second.text_store->SetImeKeyEventDispatcher(ime_key_event_dispatcher);
  }
}

void TSFBridgeImpl::RemoveImeKeyEventDispatcher(
    ImeKeyEventDispatcher* ime_key_event_dispatcher) {
  DCHECK(base::CurrentUIThread::IsSet());
  DCHECK(IsInitialized());

  for (TSFDocumentMap::iterator it = tsf_document_map_.begin();
       it != tsf_document_map_.end(); ++it) {
    if (it->second.text_store.get() == nullptr)
      continue;
    it->second.text_store->RemoveImeKeyEventDispatcher(
        ime_key_event_dispatcher);
  }
}

bool TSFBridgeImpl::IsInputLanguageCJK() {
  // See the following article about how LANGID in HKL is determined.
  // https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getkeyboardlayout
  LANGID lang_locale =
      PRIMARYLANGID(LOWORD(HandleToLong(GetKeyboardLayout(0))));
  return lang_locale == LANG_CHINESE || lang_locale == LANG_JAPANESE ||
         lang_locale == LANG_KOREAN;
}

TextInputClient* TSFBridgeImpl::GetFocusedTextInputClient() const {
  return client_;
}

void TSFBridgeImpl::OnUrlChanged() {
  TSFDocument* document = GetAssociatedDocument();
  if (!document || !document->text_store) {
    return;
  }
  document->text_store->MaybeSendOnUrlChanged();
}

Microsoft::WRL::ComPtr<ITfThreadMgr> TSFBridgeImpl::GetThreadManager() {
  DCHECK(base::CurrentUIThread::IsSet());
  DCHECK(IsInitialized());
  return thread_manager_;
}

HRESULT TSFBridgeImpl::CreateDocumentManager(TSFTextStore* text_store,
                                             ITfDocumentMgr** document_manager,
                                             ITfContext** context,
                                             DWORD* source_cookie,
                                             DWORD* key_trace_sink_cookie,
                                             DWORD* language_profile_cookie) {
  HRESULT hr = thread_manager_->CreateDocumentMgr(document_manager);
  if (FAILED(hr)) {
    DVLOG(1) << "Failed to create Document Manager.";
    return hr;
  }

  if (!text_store || !source_cookie || !key_trace_sink_cookie ||
      !language_profile_cookie) {
    return S_OK;
  }

  DWORD edit_cookie = TF_INVALID_EDIT_COOKIE;
  hr = (*document_manager)
           ->CreateContext(client_id_, 0,
                           static_cast<ITextStoreACP*>(text_store), context,
                           &edit_cookie);
  if (FAILED(hr)) {
    DVLOG(1) << "Failed to create Context.";
    return hr;
  }

  hr = (*document_manager)->Push(*context);
  if (FAILED(hr)) {
    DVLOG(1) << "Failed to push context.";
    return hr;
  }

  Microsoft::WRL::ComPtr<ITfSource> source;
  hr = (*context)->QueryInterface(IID_PPV_ARGS(&source));
  if (FAILED(hr)) {
    DVLOG(1) << "Failed to get source.";
    return hr;
  }

  hr = source->AdviseSink(IID_ITfTextEditSink,
                          static_cast<ITfTextEditSink*>(text_store),
                          source_cookie);
  if (FAILED(hr)) {
    DVLOG(1) << "AdviseSink failed.";
    return hr;
  }

  Microsoft::WRL::ComPtr<ITfSource> source_ITfThreadMgr;
  hr = thread_manager_->QueryInterface(IID_PPV_ARGS(&source_ITfThreadMgr));
  if (FAILED(hr)) {
    DVLOG(1) << "Failed to get source_ITfThreadMgr.";
    return hr;
  }

  hr = source_ITfThreadMgr->AdviseSink(
      IID_ITfKeyTraceEventSink, static_cast<ITfKeyTraceEventSink*>(text_store),
      key_trace_sink_cookie);
  if (FAILED(hr)) {
    DVLOG(1) << "AdviseSink for ITfKeyTraceEventSink failed.";
    return hr;
  }

  Microsoft::WRL::ComPtr<ITfSource> language_source;
  hr =
      input_processor_profiles_->QueryInterface(IID_PPV_ARGS(&language_source));
  if (FAILED(hr)) {
    DVLOG(1) << "Failed to get source_ITfInputProcessorProfiles.";
    return hr;
  }

  hr = language_source->AdviseSink(IID_ITfLanguageProfileNotifySink,
                                   static_cast<ITfTextEditSink*>(text_store),
                                   language_profile_cookie);
  if (FAILED(hr)) {
    DVLOG(1) << "AdviseSink for language profile notify sink failed.";
    return hr;
  }

  if (*source_cookie == TF_INVALID_COOKIE) {
    DVLOG(1) << "The result of cookie is invalid.";
    return E_FAIL;
  }
  return S_OK;
}

HRESULT TSFBridgeImpl::InitializeDocumentMapInternal() {
  const TextInputType kTextInputTypes[] = {
      TEXT_INPUT_TYPE_NONE,      TEXT_INPUT_TYPE_TEXT,
      TEXT_INPUT_TYPE_PASSWORD,  TEXT_INPUT_TYPE_SEARCH,
      TEXT_INPUT_TYPE_EMAIL,     TEXT_INPUT_TYPE_NUMBER,
      TEXT_INPUT_TYPE_TELEPHONE, TEXT_INPUT_TYPE_URL,
      TEXT_INPUT_TYPE_TEXT_AREA,
  };
  // Query TSF for empty TSF text store support, introduced with Windows 11.
  // If support is present, as indicated by successful return of an interface
  // for the IID value GUID_COMPARTMENT_EMPTYCONTEXT, we use a dummy/empty Text
  // store when there is no text.
  Microsoft::WRL::ComPtr<IUnknown> flag_empty_context;
  HRESULT res = thread_manager_->QueryInterface(GUID_COMPARTMENT_EMPTYCONTEXT,
                                                &flag_empty_context);
  if (SUCCEEDED(res)) {
    empty_tsf_support_ = true;
  }

  for (size_t i = 0; i < std::size(kTextInputTypes); ++i) {
    const TextInputType input_type = kTextInputTypes[i];
    Microsoft::WRL::ComPtr<ITfContext> context;
    Microsoft::WRL::ComPtr<ITfDocumentMgr> document_manager;
    DWORD source_cookie = TF_INVALID_COOKIE;
    DWORD key_trace_sink_cookie = TF_INVALID_COOKIE;
    DWORD language_profile_cookie = TF_INVALID_COOKIE;
    // Use a null text store if empty tsf text store is not supported.
    const bool use_null_text_store =
        (input_type == TEXT_INPUT_TYPE_NONE && !empty_tsf_support_);
    DWORD* source_cookie_ptr = use_null_text_store ? nullptr : &source_cookie;
    DWORD* key_trace_sink_cookie_ptr =
        use_null_text_store ? nullptr : &key_trace_sink_cookie;
    DWORD* language_profile_cookie_ptr =
        use_null_text_store ? nullptr : &language_profile_cookie;
    scoped_refptr<TSFTextStore> text_store =
        use_null_text_store ? nullptr : new TSFTextStore();
    if (text_store && input_type != TEXT_INPUT_TYPE_NONE) {
      // No need to initialize for TEXT_INPUT_TYPE_NONE.
      HRESULT hr = text_store->Initialize();
      if (FAILED(hr))
        return hr;
    }
    HRESULT hr = CreateDocumentManager(
        text_store.get(), &document_manager, &context, source_cookie_ptr,
        key_trace_sink_cookie_ptr, language_profile_cookie_ptr);
    if (FAILED(hr))
      return hr;
    if (input_type == TEXT_INPUT_TYPE_PASSWORD ||
        (empty_tsf_support_ && input_type == TEXT_INPUT_TYPE_NONE)) {
      // Disable context for TEXT_INPUT_TYPE_NONE, if empty text store is
      // supported.
      hr = InitializeDisabledContext(context.Get());
      if (FAILED(hr))
        return hr;
    }
    tsf_document_map_[input_type].text_store = text_store;
    tsf_document_map_[input_type].document_manager = document_manager;
    tsf_document_map_[input_type].source_cookie = source_cookie;
    tsf_document_map_[input_type].key_trace_sink_cookie = key_trace_sink_cookie;
    tsf_document_map_[input_type].language_profile_cookie =
        language_profile_cookie;
    if (text_store)
      text_store->OnContextInitialized(context.Get());

    // Set the flag for empty text store.
    if (text_store && input_type == TEXT_INPUT_TYPE_NONE) {
      text_store->UseEmptyTextStore(empty_tsf_support_);
    }
  }
  return S_OK;
}

HRESULT TSFBridgeImpl::InitializeDisabledContext(ITfContext* context) {
  Microsoft::WRL::ComPtr<ITfCompartmentMgr> compartment_mgr;
  HRESULT hr = context->QueryInterface(IID_PPV_ARGS(&compartment_mgr));
  if (FAILED(hr)) {
    DVLOG(1) << "Failed to get CompartmentMgr.";
    return hr;
  }

  Microsoft::WRL::ComPtr<ITfCompartment> disabled_compartment;
  hr = compartment_mgr->GetCompartment(GUID_COMPARTMENT_KEYBOARD_DISABLED,
                                       &disabled_compartment);
  if (FAILED(hr)) {
    DVLOG(1) << "Failed to get keyboard disabled compartment.";
    return hr;
  }

  base::win::ScopedVariant variant;
  variant.Set(1);
  hr = disabled_compartment->SetValue(client_id_, variant.ptr());
  if (FAILED(hr)) {
    DVLOG(1) << "Failed to disable the DocumentMgr.";
    return hr;
  }

  Microsoft::WRL::ComPtr<ITfCompartment> empty_context;
  hr = compartment_mgr->GetCompartment(GUID_COMPARTMENT_EMPTYCONTEXT,
                                       &empty_context);
  if (FAILED(hr)) {
    DVLOG(1) << "Failed to get empty context compartment.";
    return hr;
  }
  base::win::ScopedVariant empty_context_variant;
  empty_context_variant.Set(static_cast<int32_t>(1));
  hr = empty_context->SetValue(client_id_, empty_context_variant.ptr());
  if (FAILED(hr)) {
    DVLOG(1) << "Failed to set empty context.";
    return hr;
  }

  return S_OK;
}

bool TSFBridgeImpl::IsFocused(ITfDocumentMgr* document_manager) {
  if (!IsInitialized()) {
    // Hasn't been initialized yet. Return false.
    return false;
  }
  Microsoft::WRL::ComPtr<ITfDocumentMgr> focused_document_manager;
  if (FAILED(thread_manager_->GetFocus(&focused_document_manager)))
    return false;
  return focused_document_manager.Get() == document_manager;
}

bool TSFBridgeImpl::IsInitialized() {
  return client_id_ != TF_CLIENTID_NULL;
}

void TSFBridgeImpl::UpdateAssociateFocus() {
  if (!IsInitialized()) {
    // Hasn't been initialized yet. Do nothing.
    return;
  }
  if (attached_window_handle_ == nullptr)
    return;
  TSFDocument* document = GetAssociatedDocument();
  if (document == nullptr) {
    ClearAssociateFocus();
    return;
  }
  // NOTE: ITfThreadMgr::AssociateFocus does not increment the ref count of
  // the document manager to be attached. It is our responsibility to make sure
  // the attached document manager will not be destroyed while it is attached.
  // This should be true as long as TSFBridge::Shutdown() is called late phase
  // of UI thread shutdown.
  // AssociateFocus calls SetFocus on the document manager internally
  Microsoft::WRL::ComPtr<ITfDocumentMgr> previous_focus;
  thread_manager_->AssociateFocus(attached_window_handle_,
                                  document->document_manager.Get(),
                                  &previous_focus);
}

void TSFBridgeImpl::ClearAssociateFocus() {
  if (!IsInitialized()) {
    // Hasn't been initialized yet. Do nothing.
    return;
  }
  if (attached_window_handle_ == nullptr)
    return;
  Microsoft::WRL::ComPtr<ITfDocumentMgr> previous_focus;
  thread_manager_->AssociateFocus(attached_window_handle_, nullptr,
                                  &previous_focus);
}

TSFBridgeImpl::TSFDocument* TSFBridgeImpl::GetAssociatedDocument() {
  if (!client_)
    return nullptr;
  TSFDocumentMap::iterator it = tsf_document_map_.find(input_type_);
  if (it == tsf_document_map_.end()) {
    it = tsf_document_map_.find(TEXT_INPUT_TYPE_TEXT);
    // This check is necessary because it's possible that we failed to
    // initialize |tsf_document_map_| and it has no TEXT_INPUT_TYPE_TEXT.
    if (it == tsf_document_map_.end())
      return nullptr;
  }
  return &it->second;
}

base::ThreadLocalOwnedPointer<TSFBridge>& GetThreadLocalTSFBridge() {
  static base::NoDestructor<base::ThreadLocalOwnedPointer<TSFBridge>>
      tsf_bridge;
  return *tsf_bridge;
}

}  // namespace

// TsfBridge  -----------------------------------------------------------------

// static
HRESULT TSFBridge::Initialize() {
  TRACE_EVENT0("ime", "TSFBridge::Initialize");
  if (!base::CurrentUIThread::IsSet()) {
    return E_FAIL;
  }

  if (GetThreadLocalTSFBridge().Get()) {
    return S_OK;
  }

  auto delegate = std::make_unique<TSFBridgeImpl>();
  HRESULT hr = delegate->Initialize();
  if (SUCCEEDED(hr)) {
    ReplaceThreadLocalTSFBridge(std::move(delegate));
  }
  return hr;
}

// static
void TSFBridge::InitializeForTesting() {
  if (!base::CurrentUIThread::IsSet()) {
    return;
  }
  ReplaceThreadLocalTSFBridge(std::make_unique<MockTSFBridge>());
}

// static
void TSFBridge::ReplaceThreadLocalTSFBridge(
    std::unique_ptr<TSFBridge> new_instance) {
  if (!base::CurrentUIThread::IsSet()) {
    return;
  }
  GetThreadLocalTSFBridge().Set(std::move(new_instance));
}

// static
void TSFBridge::Shutdown() {
  TRACE_EVENT0("ime", "TSFBridge::Shutdown");
  ReplaceThreadLocalTSFBridge(nullptr);
}

// static
TSFBridge* TSFBridge::GetInstance() {
  return base::CurrentUIThread::IsSet() ? GetThreadLocalTSFBridge().Get()
                                        : nullptr;
}

}  // namespace ui