// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/ui/webui/ash/emoji/emoji_page_handler.h"
#include "ash/constants/ash_features.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/public/cpp/system/toast_data.h"
#include "ash/public/cpp/system/toast_manager.h"
#include "base/json/values_util.h"
#include "base/memory/raw_ptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/strcat.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/time.h"
#include "base/timer/timer.h"
#include "base/trace_event/trace_event.h"
#include "chrome/browser/ui/webui/ash/emoji/emoji_ui.h"
#include "chrome/browser/ui/webui/ash/emoji/seal_utils.h"
#include "chrome/grit/generated_resources.h"
#include "chromeos/ash/components/emoji/emoji_search.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "content/public/browser/storage_partition.h"
#include "ui/base/clipboard/scoped_clipboard_writer.h"
#include "ui/base/ime/ash/ime_bridge.h"
#include "ui/base/ime/input_method_observer.h"
#include "ui/base/l10n/l10n_util.h"
namespace ash {
namespace {
constexpr char kEmojiPickerToastId[] = "emoji_picker_toast";
constexpr char kPrefsHistoryTextFieldName[] = "text";
constexpr char kPrefsHistoryTimestampFieldName[] = "timestamp";
constexpr char kPrefsPreferredVariantsFieldName[] = "preferred_variants";
// Keep in sync with entry in enums.xml.
enum class EmojiVariantType {
// smaller entries only used by Chrome OS VK
kEmojiPickerBase = 4,
kEmojiPickerVariant = 5,
kEmojiPickerGifInserted = 6,
kEmojiPickerGifCopied = 7,
kMaxValue = kEmojiPickerGifCopied,
};
void LogInsertEmoji(bool is_variant, int16_t search_length) {
EmojiVariantType insert_value = is_variant
? EmojiVariantType::kEmojiPickerVariant
: EmojiVariantType::kEmojiPickerBase;
base::UmaHistogramEnumeration("InputMethod.SystemEmojiPicker.TriggerType",
insert_value);
base::UmaHistogramCounts100("InputMethod.SystemEmojiPicker.SearchLength",
search_length);
}
void LogInsertGif(bool is_inserted) {
EmojiVariantType insert_value = is_inserted
? EmojiVariantType::kEmojiPickerGifInserted
: EmojiVariantType::kEmojiPickerGifCopied;
base::UmaHistogramEnumeration("InputMethod.SystemEmojiPicker.TriggerType",
insert_value);
}
void LogInsertEmojiDelay(base::TimeDelta delay) {
base::UmaHistogramMediumTimes("InputMethod.SystemEmojiPicker.Delay", delay);
}
void LogLoadTime(base::TimeDelta delay) {
base::UmaHistogramMediumTimes("InputMethod.SystemEmojiPicker.LoadTime",
delay);
}
void LogInsertionLatency(base::TimeDelta delay) {
base::UmaHistogramTimes("InputMethod.SystemEmojiPicker.InsertionLatency",
delay);
}
void CopyEmojiToClipboard(const std::string& emoji_to_copy) {
if (base::FeatureList::IsEnabled(features::kImeSystemEmojiPickerClipboard)) {
auto clipboard = std::make_unique<ui::ScopedClipboardWriter>(
ui::ClipboardBuffer::kCopyPaste, nullptr);
clipboard->WriteText(base::UTF8ToUTF16(emoji_to_copy));
}
}
std::string BuildGifHTML(const GURL& gif) {
// Referrer-Policy is used to prevent Tenor from getting information about
// where the GIFs are being used.
return base::StrCat(
{"<img src=\"", gif.spec(), "\" referrerpolicy=\"no-referrer\">"});
}
void CopyGifToClipboard(const GURL& gif_to_copy) {
if (!gif_to_copy.is_valid()) {
return;
}
// Overwrite the clipboard data with the GIF url.
auto clipboard = std::make_unique<ui::ScopedClipboardWriter>(
ui::ClipboardBuffer::kCopyPaste);
clipboard->WriteHTML(base::UTF8ToUTF16(BuildGifHTML(gif_to_copy)), "");
// Show a toast that says "GIF not supported. Copied to clipboard.".
ToastManager::Get()->Show(ToastData(
kEmojiPickerToastId, ToastCatalogName::kCopyGifToClipboardAction,
l10n_util::GetStringUTF16(IDS_ASH_EMOJI_PICKER_COPY_GIF_TO_CLIPBOARD)));
}
std::string ConvertCategoryToPrefString(
emoji_picker::mojom::Category category) {
switch (category) {
case emoji_picker::mojom::Category::kEmojis:
return "emoji";
case emoji_picker::mojom::Category::kSymbols:
return "symbol";
case emoji_picker::mojom::Category::kEmoticons:
return "emoticon";
case emoji_picker::mojom::Category::kGifs:
return "gif";
}
}
} // namespace
// Used to insert a gif / emoji after WebUI handler is destroyed, before
// self-constructing.
class InsertObserver : public ui::InputMethodObserver {
public:
explicit InsertObserver(ui::InputMethod* ime) : ime_(ime) {
start_time_ = base::TimeTicks::Now();
delete_timer_.Start(
FROM_HERE, base::Seconds(1),
base::BindOnce(&InsertObserver::DestroySelf, base::Unretained(this)));
}
~InsertObserver() override { ime_->RemoveObserver(this); }
virtual void PerformInsert(ui::TextInputClient* input_client) = 0;
virtual void PerformCopy() = 0;
void OnTextInputStateChanged(const ui::TextInputClient* client) override {
focus_change_count_++;
// At least 2 focus changes - 1 for loss of focus in emoji picker, second
// for focusing in the new text field.
// And in lacros, we may expect third change to correct text input type (
// from initial value to actual correct value).
// You would expect this to fail if the emoji picker window does not have
// focus in the text field, but waiting for at least 2 focus changes is
// still correct behavior.
if (focus_change_count_ >= 2) {
// Need to get the client via the IME as InsertText is non-const.
// Can't use this->ime_ either as it may not be active, want to ensure
// that we get the active IME.
ui::InputMethod* input_method =
IMEBridge::Get()->GetInputContextHandler()->GetInputMethod();
if (!input_method) {
return;
}
ui::TextInputClient* input_client = input_method->GetTextInputClient();
if (!input_client) {
return;
}
if (input_client->GetTextInputType() ==
ui::TextInputType::TEXT_INPUT_TYPE_NONE) {
// In some clients (e.g. Sheets), there is an extra focus before the
// "real" text input field.
focus_change_count_--;
return;
}
PerformInsert(input_client);
if (this->inserted_) {
DestroySelf();
}
return;
}
}
void OnFocus() override {}
void OnBlur() override {}
void OnCaretBoundsChanged(const ui::TextInputClient* client) override {}
void OnInputMethodDestroyed(const ui::InputMethod* client) override {}
protected:
void MarkInserted() {
this->inserted_ = true;
LogInsertionLatency(base::TimeTicks::Now() - start_time_);
}
private:
void DestroySelf() {
if (!inserted_) {
PerformCopy();
}
delete this;
}
int focus_change_count_ = 0;
base::OneShotTimer delete_timer_;
raw_ptr<ui::InputMethod, LeakedDanglingUntriaged> ime_;
bool inserted_ = false;
base::TimeTicks start_time_;
};
// Used to insert an emoji after WebUI handler is destroyed, before
// self-destructing.
class EmojiObserver : public InsertObserver {
public:
explicit EmojiObserver(const std::string& emoji_to_insert,
ui::InputMethod* ime)
: InsertObserver(ime), emoji_to_insert_(emoji_to_insert) {}
void PerformInsert(ui::TextInputClient* input_client) override {
if (input_client->GetTextInputType() ==
ui::TextInputType::TEXT_INPUT_TYPE_NONE) {
// In some clients (e.g. Sheets), there is an extra focus before the
// "real" text input field. so we skip this insertion.
return;
}
input_client->InsertText(
base::UTF8ToUTF16(emoji_to_insert_),
ui::TextInputClient::InsertTextCursorBehavior::kMoveCursorAfterText);
MarkInserted();
}
void PerformCopy() override { CopyEmojiToClipboard(emoji_to_insert_); }
private:
std::string emoji_to_insert_;
};
// Used to insert a gif after WebUI handler is destroyed, before
// self-destructing.
class GifObserver : public InsertObserver {
public:
explicit GifObserver(const GURL& gif_to_insert, ui::InputMethod* ime)
: InsertObserver(ime), gif_to_insert_(gif_to_insert) {}
void PerformInsert(ui::TextInputClient* input_client) override {
if (input_client->CanInsertImage()) {
input_client->InsertImage(gif_to_insert_);
MarkInserted();
LogInsertGif(/*is_inserted=*/true);
}
}
void PerformCopy() override {
CopyGifToClipboard(gif_to_insert_);
LogInsertGif(/*is_inserted=*/false);
}
private:
GURL gif_to_insert_;
};
EmojiPageHandler::EmojiPageHandler(
mojo::PendingReceiver<emoji_picker::mojom::PageHandler> receiver,
content::WebUI* web_ui,
EmojiUI* webui_controller,
bool incognito_mode,
bool no_text_field,
emoji_picker::mojom::Category initial_category,
const std::string& initial_query)
: receiver_(this, std::move(receiver)),
webui_controller_(webui_controller),
incognito_mode_(incognito_mode),
no_text_field_(no_text_field),
initial_category_(initial_category),
initial_query_(initial_query),
profile_(Profile::FromWebUI(web_ui)) {
// There are two conditions to control the GIF support:
// 1. Feature flag is turned on.
// 2. For managed users, the policy is turned on.
gif_support_enabled_ =
base::FeatureList::IsEnabled(features::kImeSystemEmojiPickerGIFSupport) &&
(profile_->GetPrefs()->IsManagedPreference(
prefs::kEmojiPickerGifSupportEnabled)
? profile_->GetPrefs()->GetBoolean(
prefs::kEmojiPickerGifSupportEnabled)
: true);
url_loader_factory_ = profile_->GetDefaultStoragePartition()
->GetURLLoaderFactoryForBrowserProcess();
}
EmojiPageHandler::~EmojiPageHandler() {}
void EmojiPageHandler::ShowUI() {
auto embedder = webui_controller_->embedder();
// Embedder may not exist in some cases (e.g. user browses to
// chrome://emoji-picker directly rather than using right click on
// text field -> emoji).
if (embedder) {
embedder->ShowUI();
}
shown_time_ = base::TimeTicks::Now();
}
void EmojiPageHandler::IsIncognitoTextField(
IsIncognitoTextFieldCallback callback) {
std::move(callback).Run(incognito_mode_);
}
void EmojiPageHandler::GetFeatureList(GetFeatureListCallback callback) {
std::vector<emoji_picker::mojom::Feature> enabled_features;
if (base::FeatureList::IsEnabled(
features::kImeSystemEmojiPickerSearchExtension)) {
enabled_features.push_back(
emoji_picker::mojom::Feature::EMOJI_PICKER_SEARCH_EXTENSION);
}
if (gif_support_enabled_) {
enabled_features.push_back(
emoji_picker::mojom::Feature::EMOJI_PICKER_GIF_SUPPORT);
}
if (base::FeatureList::IsEnabled(features::kImeSystemEmojiPickerMojoSearch)) {
enabled_features.push_back(
emoji_picker::mojom::Feature::EMOJI_PICKER_MOJO_SEARCH);
}
if (SealUtils::ShouldEnable()) {
enabled_features.push_back(
emoji_picker::mojom::Feature::EMOJI_PICKER_SEAL_SUPPORT);
}
if (base::FeatureList::IsEnabled(
features::kImeSystemEmojiPickerVariantGrouping)) {
enabled_features.push_back(
emoji_picker::mojom::Feature::EMOJI_PICKER_VARIANT_GROUPING_SUPPORT);
}
std::move(callback).Run(enabled_features);
}
void EmojiPageHandler::GetCategories(GetCategoriesCallback callback) {
gif_tenor_api_fetcher_.FetchCategories(std::move(callback),
url_loader_factory_);
}
void EmojiPageHandler::GetFeaturedGifs(const std::optional<std::string>& pos,
GetFeaturedGifsCallback callback) {
gif_tenor_api_fetcher_.FetchFeaturedGifs(std::move(callback),
url_loader_factory_, pos);
}
void EmojiPageHandler::SearchGifs(const std::string& query,
const std::optional<std::string>& pos,
SearchGifsCallback callback) {
gif_tenor_api_fetcher_.FetchGifSearch(std::move(callback),
url_loader_factory_, query, pos);
}
void EmojiPageHandler::GetGifsByIds(const std::vector<std::string>& ids,
GetGifsByIdsCallback callback) {
gif_tenor_api_fetcher_.FetchGifsByIds(std::move(callback),
url_loader_factory_, ids);
}
void EmojiPageHandler::InsertEmoji(const std::string& emoji_to_insert,
bool is_variant,
int16_t search_length) {
LogInsertEmoji(is_variant, search_length);
LogInsertEmojiDelay(base::TimeTicks::Now() - shown_time_);
// In theory, we are returning focus to the input field where the user
// originally selected emoji. However, the input field may not exist anymore
// e.g. JS has mutated the web page while emoji picker was open, so check
// that a valid input client is available as part of inserting the emoji.
ui::InputMethod* input_method =
IMEBridge::Get()->GetInputContextHandler()->GetInputMethod();
if (!input_method) {
DLOG(WARNING) << "no input_method found";
CopyEmojiToClipboard(emoji_to_insert);
return;
}
if (no_text_field_) {
CopyEmojiToClipboard(emoji_to_insert);
return;
}
// It seems like this might leak EmojiObserver, but the EmojiObserver
// destroys itself on a timer (complex behavior needed since the observer
// needs to outlive the page handler)
input_method->AddObserver(new EmojiObserver(emoji_to_insert, input_method));
// By hiding the emoji picker, we restore focus to the original text field
// so we can insert the text.
auto embedder = webui_controller_->embedder();
if (embedder) {
embedder->CloseUI();
}
}
void EmojiPageHandler::InsertGif(const GURL& gif) {
if (!gif.is_valid()) {
return;
}
ui::InputMethod* input_method =
IMEBridge::Get()->GetInputContextHandler()->GetInputMethod();
if (!input_method) {
DLOG(WARNING) << "no input_method found";
CopyGifToClipboard(gif);
LogInsertGif(/*is_inserted=*/false);
return;
}
if (no_text_field_) {
CopyGifToClipboard(gif);
LogInsertGif(/*is_inserted=*/false);
return;
}
// The GifObserver here will self-destroy.
input_method->AddObserver(new GifObserver(gif, input_method));
// By hiding the emoji picker, we restore focus to the original text field.
auto embedder = webui_controller_->embedder();
if (embedder) {
embedder->CloseUI();
}
}
void EmojiPageHandler::OnUiFullyLoaded() {
LogLoadTime(base::TimeTicks::Now() - shown_time_);
}
void EmojiPageHandler::GetInitialCategory(GetInitialCategoryCallback callback) {
std::move(callback).Run(initial_category_);
}
void EmojiPageHandler::GetInitialQuery(GetInitialQueryCallback callback) {
std::move(callback).Run(initial_query_);
}
void EmojiPageHandler::UpdateHistoryInPrefs(
emoji_picker::mojom::Category category,
std::vector<emoji_picker::mojom::HistoryItemPtr> history) {
base::Value::List history_value;
for (const auto& item : history) {
history_value.Append(base::Value::Dict()
.Set(kPrefsHistoryTextFieldName, item->emoji)
.Set(kPrefsHistoryTimestampFieldName,
base::TimeToValue(item->timestamp)));
}
ScopedDictPrefUpdate update(profile_->GetPrefs(), prefs::kEmojiPickerHistory);
update->Set(ConvertCategoryToPrefString(category), std::move(history_value));
}
void EmojiPageHandler::UpdatePreferredVariantsInPrefs(
std::vector<emoji_picker::mojom::EmojiVariantPtr> preferred_variants) {
base::Value::Dict value;
for (const auto& variant : preferred_variants) {
value.Set(variant->base, variant->variant);
}
ScopedDictPrefUpdate update(profile_->GetPrefs(),
prefs::kEmojiPickerPreferences);
update->Set(kPrefsPreferredVariantsFieldName, std::move(value));
}
void EmojiPageHandler::GetHistoryFromPrefs(
emoji_picker::mojom::Category category,
GetHistoryFromPrefsCallback callback) {
if (profile_ == nullptr || profile_->GetPrefs() == nullptr) {
std::move(callback).Run({});
return;
}
const base::Value::List* history =
profile_->GetPrefs()
->GetDict(prefs::kEmojiPickerHistory)
.FindList(ConvertCategoryToPrefString(category));
if (history == nullptr) {
std::move(callback).Run({});
return;
}
std::vector<emoji_picker::mojom::HistoryItemPtr> results;
for (const auto& it : *history) {
const base::Value::Dict* value_dict = it.GetIfDict();
if (value_dict == nullptr) {
continue;
}
const std::string* text =
value_dict->FindString(kPrefsHistoryTextFieldName);
std::optional<base::Time> timestamp =
base::ValueToTime(value_dict->Find(kPrefsHistoryTimestampFieldName));
if (text != nullptr) {
results.push_back(emoji_picker::mojom::HistoryItem::New(
*text, timestamp.has_value() ? *timestamp : base::Time::UnixEpoch()));
}
}
std::move(callback).Run(std::move(results));
}
} // namespace ash