blob: 3bf092cfcc497babb7691ba3f39c8b23ca074902 [file] [log] [blame]
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ash/picker/picker_controller.h"
#include <memory>
#include <string>
#include <string_view>
#include <utility>
#include <variant>
#include "ash/constants/ash_switches.h"
#include "ash/picker/model/picker_search_results.h"
#include "ash/picker/picker_asset_fetcher.h"
#include "ash/picker/picker_asset_fetcher_impl.h"
#include "ash/picker/picker_copy_media.h"
#include "ash/picker/picker_insert_media_request.h"
#include "ash/picker/picker_search_controller.h"
#include "ash/picker/views/picker_view.h"
#include "ash/picker/views/picker_view_delegate.h"
#include "ash/public/cpp/ash_web_view_factory.h"
#include "ash/public/cpp/picker/picker_client.h"
#include "ash/public/cpp/picker/picker_search_result.h"
#include "ash/wm/window_util.h"
#include "base/check.h"
#include "base/check_is_test.h"
#include "base/command_line.h"
#include "base/functional/bind.h"
#include "base/functional/overloaded.h"
#include "base/hash/sha1.h"
#include "ui/aura/window.h"
#include "ui/base/ime/ash/ime_bridge.h"
#include "ui/base/ime/ash/input_method_manager.h"
#include "ui/base/ime/input_method.h"
#include "ui/display/screen.h"
#include "ui/gfx/geometry/point.h"
#include "ui/gfx/geometry/rect.h"
namespace ash {
namespace {
bool g_should_check_key = true;
// The hash value for the feature key of the Picker feature, used for
// development.
constexpr std::string_view kPickerFeatureDevKeyHash(
"\xE1\xC0\x09\x7F\xBE\x03\xBF\x48\xA7\xA0\x30\x53\x07\x4F\xFB\xC5\x6D\xD4"
"\x22\x5F",
base::kSHA1Length);
// The hash value for the feature key of the Picker feature, used in some tests.
constexpr std::string_view kPickerFeatureTestKeyHash(
"\xE7\x2C\x99\xD7\x99\x89\xDB\xA5\x9D\x06\x4A\xED\xDF\xE5\x30\xA7\x8C\x76"
"\x00\x89",
base::kSHA1Length);
// Time from when the insert is issued and when we give up inserting.
constexpr base::TimeDelta kInsertMediaTimeout = base::Seconds(2);
// Time from when a start starts to when the first set of results are published.
// TODO: b/325195938 - Lower this to 200ms without affecting results.
constexpr base::TimeDelta kBurnInPeriod = base::Milliseconds(400);
enum class PickerFeatureKeyType { kNone, kDev, kTest };
PickerFeatureKeyType MatchPickerFeatureKeyHash() {
// Command line looks like:
// out/Default/chrome --user-data-dir=/tmp/tmp123
// --picker-feature-key="INSERT KEY HERE" --enable-features=PickerFeature
const std::string provided_key_hash = base::SHA1HashString(
base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
switches::kPickerFeatureKey));
if (provided_key_hash == kPickerFeatureDevKeyHash) {
return PickerFeatureKeyType::kDev;
}
if (provided_key_hash == kPickerFeatureTestKeyHash) {
return PickerFeatureKeyType::kTest;
}
return PickerFeatureKeyType::kNone;
}
// Gets the current caret bounds in universal screen coordinates in DIP. Returns
// an empty rect if there is no active caret or the caret bounds can't be
// determined (e.g. no focused input field).
gfx::Rect GetCaretBounds() {
const ui::InputMethod* input_method =
IMEBridge::Get()->GetInputContextHandler()->GetInputMethod();
if (!input_method || !input_method->GetTextInputClient()) {
return gfx::Rect();
}
return input_method->GetTextInputClient()->GetCaretBounds();
}
// Gets the current cursor point in universal screen coordinates in DIP.
gfx::Point GetCursorPoint() {
return display::Screen::GetScreen()->GetCursorScreenPoint();
}
// Gets the bounds of the current focused window in universal screen coordinates
// in DIP. Returns an empty rect if there is no currently focused window.
gfx::Rect GetFocusedWindowBounds() {
return window_util::GetFocusedWindow()
? window_util::GetFocusedWindow()->GetBoundsInScreen()
: gfx::Rect();
}
PickerInsertMediaRequest::MediaData ResultToInsertMediaData(
const PickerSearchResult& result) {
return std::visit(
base::Overloaded{
[](const PickerSearchResult::TextData& data) {
return PickerInsertMediaRequest::MediaData::Text(data.text);
},
[](const PickerSearchResult::EmojiData& data) {
return PickerInsertMediaRequest::MediaData::Text(data.emoji);
},
[](const PickerSearchResult::SymbolData& data) {
return PickerInsertMediaRequest::MediaData::Text(data.symbol);
},
[](const PickerSearchResult::EmoticonData& data) {
return PickerInsertMediaRequest::MediaData::Text(data.emoticon);
},
[](const PickerSearchResult::GifData& data) {
return PickerInsertMediaRequest::MediaData::Image(data.url);
},
[](const PickerSearchResult::BrowsingHistoryData& data) {
return PickerInsertMediaRequest::MediaData::Link(data.url);
},
},
result.data());
}
void MaybeCopyMediaToClipboard(const PickerSearchResult& result) {
if (const auto* gif =
std::get_if<PickerSearchResult::GifData>(&result.data())) {
CopyGifMediaToClipboard(gif->url, gif->content_description);
}
}
} // namespace
PickerController::PickerController() {
asset_fetcher_ = std::make_unique<PickerAssetFetcherImpl>(base::BindRepeating(
&PickerController::DownloadGifToString, weak_ptr_factory_.GetWeakPtr()));
if (auto* manager = ash::input_method::InputMethodManager::Get()) {
keyboard_observation_.Observe(manager->GetImeKeyboard());
}
}
PickerController::~PickerController() {
// `widget_` depends on `this`. Destroy the widget synchronously to avoid a
// dangling pointer.
if (widget_) {
widget_->CloseNow();
}
}
bool PickerController::IsFeatureKeyMatched() {
if (!g_should_check_key) {
return true;
}
if (MatchPickerFeatureKeyHash() == PickerFeatureKeyType::kNone) {
LOG(ERROR) << "Provided feature key does not match with the expected one.";
return false;
}
return true;
}
void PickerController::DisableFeatureKeyCheckForTesting() {
CHECK_IS_TEST();
g_should_check_key = false;
}
void PickerController::SetClient(PickerClient* client) {
client_ = client;
if (client_ == nullptr) {
search_controller_ = nullptr;
} else {
search_controller_ =
std::make_unique<PickerSearchController>(client_, kBurnInPeriod);
}
}
void PickerController::ToggleWidget(
const base::TimeTicks trigger_event_timestamp) {
CHECK(client_);
if (widget_) {
widget_->Close();
} else {
widget_ = PickerView::CreateWidget(GetCaretBounds(), GetCursorPoint(),
GetFocusedWindowBounds(), this,
trigger_event_timestamp);
widget_->Show();
feature_usage_metrics_.StartUsage();
widget_observation_.Observe(widget_.get());
}
}
std::unique_ptr<AshWebView> PickerController::CreateWebView(
const AshWebView::InitParams& params) {
return client_->CreateWebView(params);
}
void PickerController::GetResultsForCategory(PickerCategory category,
SearchResultsCallback callback) {
// TODO: b/316936620 - Get actual results for the category.
callback.Run(PickerSearchResults({{
PickerSearchResults::Section(u"Recently used",
{{PickerSearchResult::Text(u"😊")}}),
}}));
}
void PickerController::StartSearch(const std::u16string& query,
std::optional<PickerCategory> category,
SearchResultsCallback callback) {
CHECK(search_controller_);
search_controller_->StartSearch(query, std::move(category),
std::move(callback));
}
void PickerController::InsertResultOnNextFocus(
const PickerSearchResult& result) {
if (!widget_) {
return;
}
ui::InputMethod* input_method = widget_->GetInputMethod();
if (input_method == nullptr) {
return;
}
// This cancels the previous request if there was one.
insert_media_request_ = std::make_unique<PickerInsertMediaRequest>(
input_method, ResultToInsertMediaData(result), kInsertMediaTimeout,
base::BindOnce(&MaybeCopyMediaToClipboard, result));
}
PickerAssetFetcher* PickerController::GetAssetFetcher() {
return asset_fetcher_.get();
}
void PickerController::OnCapsLockChanged(bool enabled) {
// TODO: b/319301963 - Remove this behaviour once the experiment is over.
ToggleWidget();
}
void PickerController::OnWidgetDestroying(views::Widget* widget) {
feature_usage_metrics_.StopUsage();
widget_observation_.Reset();
}
void PickerController::DownloadGifToString(
const GURL& url,
base::OnceCallback<void(const std::string&)> callback) {
if (!client_) {
// TODO: b/316936723 - Add better handling of errors.
std::move(callback).Run(std::string());
return;
}
std::optional<ValidGifUrl> validated_url = ValidGifUrl::Create(url);
CHECK(validated_url.has_value());
client_->DownloadGifToString(*validated_url, std::move(callback));
}
} // namespace ash