Commit 40f1bad5 authored by Justin Cohen's avatar Justin Cohen Committed by Commit Bot

Revert "Fix iOS Omnibox Clipboard Suggestions to not immediately read clipboard"

This reverts commit 0ab91ac7.

Reason for revert: Failures starting in:
https://ci.chromium.org/p/chromium/builders/ci/ios14-sdk-simulator/2281

* ClipboardRecentContentIOSTest.AddingNonStringRemovesCachedString
* ClipboardRecentContentIOSTest.PasteboardURLObsolescence
* ClipboardRecentContentIOSTest.SchemeFiltering
* ClipboardRecentContentIOSTest.SuppressedPasteboardImage
* ClipboardRecentContentIOSTest.SuppressedPasteboardText

https://logs.chromium.org/logs/chromium/buildbucket/cr-buildbucket.appspot.com/8872067354517054672/+/steps/components_unittests_iPhone_6s_14.0_on_Mac-10.15/0/stdout

Original change's description:
> Fix iOS Omnibox Clipboard Suggestions to not immediately read clipboard
> 
> The basic idea here is to factor out the match creation methods. On iOS
> 14, if the clipboard contains content, we create a blank match of the
> correct type (one without any destination_url). Then, when the match
> is selected for opening, we can ask the clipboard_provider for a new
> match using the proper text, and open that one instead.
> 
> For other platforms, we can keep using the old method, where we access
> the clipboard immediately and construct the destination_url at match
> creation time. iOS 14 also uses this behavior if there is cached
> clipboard content.
> 
> Bug: 1105869
> Change-Id: I70d26f4bcc485e4dc9073db2257161355adc8a40
> Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2346290
> Commit-Queue: Robbie Gibson <rkgibson@google.com>
> Reviewed-by: Ted Choc <tedchoc@chromium.org>
> Reviewed-by: Justin Donnelly <jdonnelly@chromium.org>
> Reviewed-by: manuk hovanesian <manukh@chromium.org>
> Cr-Commit-Position: refs/heads/master@{#797671}

TBR=jdonnelly@chromium.org,tedchoc@chromium.org,manukh@chromium.org,rkgibson@google.com

Change-Id: I170940bf76f9e60ab0eb9d15e549fd4a62de45d3
No-Presubmit: true
No-Tree-Checks: true
No-Try: true
Bug: 1105869
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2354524Reviewed-by: default avatarJustin Cohen <justincohen@chromium.org>
Commit-Queue: Justin Cohen <justincohen@chromium.org>
Cr-Commit-Position: refs/heads/master@{#797803}
parent 03358347
......@@ -43,8 +43,18 @@ int AutocompleteClassifier::DefaultOmniboxProviders() {
#if !defined(OS_ANDROID) && !defined(OS_IOS)
// Custom search engines cannot be used on mobile.
AutocompleteProvider::TYPE_KEYWORD |
#else
#endif
#if defined(OS_ANDROID)
AutocompleteProvider::TYPE_CLIPBOARD |
#endif
#if defined(OS_IOS)
// On iOS 14, a notification appears whenever the clipboard is accessed.
// The clipboard provider accesses the clipboard every time the omnibox is
// opened. Until a better solution is found, disable the clipboard
// provider temporarily. See crbug.com/1098722.
(!base::ios::IsRunningOnIOS14OrLater()
? AutocompleteProvider::TYPE_CLIPBOARD
: 0) |
#endif
AutocompleteProvider::TYPE_ZERO_SUGGEST |
AutocompleteProvider::TYPE_ZERO_SUGGEST_LOCAL_HISTORY |
......
......@@ -320,10 +320,9 @@ AutocompleteController::AutocompleteController(
// ClipboardRecentContent can be null in iOS tests. For non-iOS, we
// create a ClipboardRecentContent as above (for both Chrome and tests).
if (ClipboardRecentContent::GetInstance()) {
clipboard_provider_ = new ClipboardProvider(
providers_.push_back(new ClipboardProvider(
provider_client_.get(), this, history_url_provider_,
ClipboardRecentContent::GetInstance());
providers_.push_back(clipboard_provider_);
ClipboardRecentContent::GetInstance()));
}
}
......
......@@ -24,7 +24,6 @@
#include "components/omnibox/browser/autocomplete_provider_listener.h"
#include "components/omnibox/browser/autocomplete_result.h"
class ClipboardProvider;
class DocumentProvider;
class HistoryURLProvider;
class KeywordProvider;
......@@ -161,7 +160,6 @@ class AutocompleteController : public AutocompleteProviderListener,
}
KeywordProvider* keyword_provider() const { return keyword_provider_; }
SearchProvider* search_provider() const { return search_provider_; }
ClipboardProvider* clipboard_provider() const { return clipboard_provider_; }
const AutocompleteInput& input() const { return input_; }
const AutocompleteResult& result() const { return result_; }
......@@ -296,8 +294,6 @@ class AutocompleteController : public AutocompleteProviderListener,
OnDeviceHeadProvider* on_device_head_provider_;
ClipboardProvider* clipboard_provider_;
// Input passed to Start.
AutocompleteInput input_;
......
......@@ -150,37 +150,16 @@ void ClipboardProvider::Start(const AutocompleteInput& input,
if (CreateImageMatch(input))
return;
bool read_clipboard_content = false;
bool read_clipboard_url;
base::Optional<AutocompleteMatch> optional_match =
CreateURLMatch(input, &read_clipboard_url);
read_clipboard_content |= read_clipboard_url;
if (!optional_match) {
bool read_clipboard_text;
optional_match = CreateTextMatch(input, &read_clipboard_text);
read_clipboard_content |= read_clipboard_text;
}
base::Optional<AutocompleteMatch> optional_match = CreateURLMatch(input);
if (!optional_match)
optional_match = CreateTextMatch(input);
if (optional_match) {
AddCreatedMatchWithTracking(input, std::move(optional_match).value(),
clipboard_content_->GetClipboardContentAge());
// The clipboard does not contain any suggestions
if (!optional_match)
return;
}
// If there was clipboard content, but no match, don't proceed. There was
// some other reason for not creating a match (e.g. copied URL but the URL was
// the same as the current URL).
if (read_clipboard_content) {
return;
}
// On iOS 14, accessing the clipboard contents shows a notification to the
// user. To avoid this, all the methods above will not check the contents and
// will return false/base::nullopt. Instead, check the existence of content
// without accessing the actual content and create blank matches.
done_ = false;
// Image matched was kicked off asynchronously, so proceed when that ends.
CheckClipboardContent(input);
AddCreatedMatchWithTracking(input, std::move(optional_match).value(),
clipboard_content_->GetClipboardContentAge());
}
void ClipboardProvider::Stop(bool clear_cached_results,
......@@ -247,6 +226,7 @@ void ClipboardProvider::AddCreatedMatchWithTracking(
current_url_suggested_times_ = 1;
}
// If the omnibox is not empty, add a default match.
// This match will be opened when the user presses "Enter".
if (!input.text().empty()) {
......@@ -267,97 +247,14 @@ void ClipboardProvider::AddCreatedMatchWithTracking(
matches_.push_back(match);
}
bool ClipboardProvider::TemplateURLSupportsTextSearch() {
TemplateURLService* url_service = client_->GetTemplateURLService();
const TemplateURL* default_url = url_service->GetDefaultSearchProvider();
if (!default_url)
return false;
DCHECK(!default_url->url().empty());
DCHECK(default_url->url_ref().IsValid(url_service->search_terms_data()));
return true;
}
bool ClipboardProvider::TemplateURLSupportsImageSearch() {
TemplateURLService* url_service = client_->GetTemplateURLService();
const TemplateURL* default_url = url_service->GetDefaultSearchProvider();
return default_url && !default_url->image_url().empty() &&
default_url->image_url_ref().IsValid(url_service->search_terms_data());
}
void ClipboardProvider::CheckClipboardContent(const AutocompleteInput& input) {
std::set<ClipboardContentType> desired_types;
desired_types.insert(ClipboardContentType::URL);
if (TemplateURLSupportsTextSearch()) {
desired_types.insert(ClipboardContentType::Text);
}
if (base::FeatureList::IsEnabled(
omnibox::kEnableClipboardProviderImageSuggestions) &&
TemplateURLSupportsImageSearch()) {
desired_types.insert(ClipboardContentType::Image);
}
// We want to get the age here because the contents of the clipboard could
// change after this point. We want the age of the contents we actually use,
// not the age of whatever's on the clipboard when the histogram is created
// (i.e when the match is created).
base::TimeDelta clipboard_contents_age =
clipboard_content_->GetClipboardContentAge();
clipboard_content_->HasRecentContentFromClipboard(
desired_types,
base::BindOnce(&ClipboardProvider::OnReceiveClipboardContent,
callback_weak_ptr_factory_.GetWeakPtr(), input,
clipboard_contents_age));
}
void ClipboardProvider::OnReceiveClipboardContent(
const AutocompleteInput& input,
base::TimeDelta clipboard_contents_age,
std::set<ClipboardContentType> matched_types) {
if (matched_types.find(ClipboardContentType::Image) != matched_types.end()) {
// The image content will be added in later. If the image is large, encoding
// the image may take some time, so just be wary whenever that step happens
// (e.g OmniboxView::OpenMatch).
AutocompleteMatch match = NewBlankImageMatch();
field_trial_triggered_ = true;
field_trial_triggered_in_session_ = true;
// Some users may be in a counterfactual study arm in which we perform all
// necessary work but do not forward the autocomplete matches.
bool in_counterfactual_group = base::GetFieldTrialParamByFeatureAsBool(
omnibox::kEnableClipboardProviderImageSuggestions,
"ClipboardProviderImageSuggestionsCounterfactualArm", false);
if (!in_counterfactual_group) {
AddCreatedMatchWithTracking(input, match, clipboard_contents_age);
listener_->OnProviderUpdate(true);
}
} else if (matched_types.find(ClipboardContentType::URL) !=
matched_types.end()) {
AutocompleteMatch match = NewBlankURLMatch();
AddCreatedMatchWithTracking(input, match, clipboard_contents_age);
listener_->OnProviderUpdate(true);
} else if (matched_types.find(ClipboardContentType::Text) !=
matched_types.end()) {
AutocompleteMatch match = NewBlankTextMatch();
AddCreatedMatchWithTracking(input, match, clipboard_contents_age);
listener_->OnProviderUpdate(true);
}
done_ = true;
}
base::Optional<AutocompleteMatch> ClipboardProvider::CreateURLMatch(
const AutocompleteInput& input,
bool* read_clipboard_content) {
*read_clipboard_content = false;
const AutocompleteInput& input) {
// The clipboard does not contain a URL worth suggesting.
base::Optional<GURL> optional_gurl =
clipboard_content_->GetRecentURLFromClipboard();
if (!optional_gurl)
return base::nullopt;
*read_clipboard_content = true;
GURL url = std::move(optional_gurl).value();
// The URL on the page is the same as the URL in the clipboard. Don't
......@@ -365,103 +262,11 @@ base::Optional<AutocompleteMatch> ClipboardProvider::CreateURLMatch(
if (url == input.current_url())
return base::nullopt;
return NewClipboardURLMatch(url);
}
base::Optional<AutocompleteMatch> ClipboardProvider::CreateTextMatch(
const AutocompleteInput& input,
bool* read_clipboard_content) {
*read_clipboard_content = false;
base::Optional<base::string16> optional_text =
clipboard_content_->GetRecentTextFromClipboard();
if (!optional_text)
return base::nullopt;
*read_clipboard_content = true;
base::string16 text = std::move(optional_text).value();
// The clipboard can contain the empty string, which shouldn't be suggested.
if (text.empty())
return base::nullopt;
// The text in the clipboard is a url. We don't want to prompt the user to
// search for a url.
if (GURL(text).is_valid())
return base::nullopt;
return NewClipboardTextMatch(text);
}
bool ClipboardProvider::CreateImageMatch(const AutocompleteInput& input) {
// Only try image match if feature is enabled
if (!base::FeatureList::IsEnabled(
omnibox::kEnableClipboardProviderImageSuggestions)) {
return false;
}
if (!clipboard_content_->HasRecentImageFromClipboard()) {
return false;
}
if (!TemplateURLSupportsImageSearch()) {
return false;
}
done_ = false;
// We want to get the age here because the contents of the clipboard could
// change after this point. We want the age of the image we actually use, not
// the age of whatever's on the clipboard when the histogram is created (i.e
// when the match is created).
base::TimeDelta clipboard_contents_age =
clipboard_content_->GetClipboardContentAge();
clipboard_content_->GetRecentImageFromClipboard(base::BindOnce(
&ClipboardProvider::CreateImageMatchCallback,
callback_weak_ptr_factory_.GetWeakPtr(), input, clipboard_contents_age));
return true;
}
void ClipboardProvider::CreateImageMatchCallback(
const AutocompleteInput& input,
const base::TimeDelta clipboard_contents_age,
base::Optional<gfx::Image> optional_image) {
if (!optional_image) {
return;
}
NewClipboardImageMatch(
optional_image.value(),
base::BindOnce(&ClipboardProvider::AddImageMatchCallback,
callback_weak_ptr_factory_.GetWeakPtr(), input,
clipboard_contents_age));
}
void ClipboardProvider::AddImageMatchCallback(
const AutocompleteInput& input,
const base::TimeDelta clipboard_contents_age,
base::Optional<AutocompleteMatch> match) {
if (!match) {
return;
}
AddCreatedMatchWithTracking(input, match.value(), clipboard_contents_age);
listener_->OnProviderUpdate(true);
done_ = true;
}
DCHECK(url.is_valid());
AutocompleteMatch ClipboardProvider::NewBlankURLMatch() {
AutocompleteMatch match(this, kClipboardMatchRelevanceScore,
IsMatchDeletionEnabled(),
AutocompleteMatchType::CLIPBOARD_URL);
match.description.assign(l10n_util::GetStringUTF16(IDS_LINK_FROM_CLIPBOARD));
if (!match.description.empty())
match.description_class.push_back({0, ACMatchClassification::NONE});
return match;
}
AutocompleteMatch ClipboardProvider::NewClipboardURLMatch(GURL url) {
DCHECK(url.is_valid());
AutocompleteMatch match = NewBlankURLMatch();
match.destination_url = url;
// Because the user did not type a related input to get this clipboard
......@@ -474,30 +279,35 @@ AutocompleteMatch ClipboardProvider::NewClipboardURLMatch(GURL url) {
match.fill_into_edit =
AutocompleteInput::FormattedStringWithEquivalentMeaning(
url, match.contents, client_->GetSchemeClassifier(), nullptr);
return match;
}
AutocompleteMatch ClipboardProvider::NewBlankTextMatch() {
AutocompleteMatch match(this, kClipboardMatchRelevanceScore,
IsMatchDeletionEnabled(),
AutocompleteMatchType::CLIPBOARD_TEXT);
match.description.assign(l10n_util::GetStringUTF16(IDS_TEXT_FROM_CLIPBOARD));
match.description.assign(l10n_util::GetStringUTF16(IDS_LINK_FROM_CLIPBOARD));
if (!match.description.empty())
match.description_class.push_back({0, ACMatchClassification::NONE});
match.transition = ui::PAGE_TRANSITION_GENERATED;
return match;
}
base::Optional<AutocompleteMatch> ClipboardProvider::NewClipboardTextMatch(
base::string16 text) {
base::Optional<AutocompleteMatch> ClipboardProvider::CreateTextMatch(
const AutocompleteInput& input) {
base::Optional<base::string16> optional_text =
clipboard_content_->GetRecentTextFromClipboard();
if (!optional_text)
return base::nullopt;
base::string16 text = std::move(optional_text).value();
// The clipboard can contain the empty string, which shouldn't be suggested.
if (text.empty())
return base::nullopt;
// The text in the clipboard is a url. We don't want to prompt the user to
// search for a url.
if (GURL(text).is_valid())
return base::nullopt;
AutocompleteMatch match = NewBlankTextMatch();
AutocompleteMatch match(this, kClipboardMatchRelevanceScore,
IsMatchDeletionEnabled(),
AutocompleteMatchType::CLIPBOARD_TEXT);
match.fill_into_edit = text;
TemplateURLService* url_service = client_->GetTemplateURLService();
......@@ -517,51 +327,65 @@ base::Optional<AutocompleteMatch> ClipboardProvider::NewClipboardTextMatch(
if (!match.contents.empty())
match.contents_class.push_back({0, ACMatchClassification::NONE});
match.description.assign(l10n_util::GetStringUTF16(IDS_TEXT_FROM_CLIPBOARD));
if (!match.description.empty())
match.description_class.push_back({0, ACMatchClassification::NONE});
match.keyword = default_url->keyword();
match.transition = ui::PAGE_TRANSITION_GENERATED;
return match;
}
AutocompleteMatch ClipboardProvider::NewBlankImageMatch() {
AutocompleteMatch match(this, kClipboardMatchRelevanceScore,
IsMatchDeletionEnabled(),
AutocompleteMatchType::CLIPBOARD_IMAGE);
bool ClipboardProvider::CreateImageMatch(const AutocompleteInput& input) {
// Only try image match if feature is enabled
if (!base::FeatureList::IsEnabled(
omnibox::kEnableClipboardProviderImageSuggestions)) {
return false;
}
match.description.assign(l10n_util::GetStringUTF16(IDS_IMAGE_FROM_CLIPBOARD));
if (!match.description.empty())
match.description_class.push_back({0, ACMatchClassification::NONE});
if (!clipboard_content_->HasRecentImageFromClipboard()) {
return false;
}
// This will end up being something like "Search for Copied Image." This may
// seem strange to use for |fill_into_edit|, but it is because iOS requires
// some text in the text field for the Enter key to work when using keyboard
// navigation.
match.fill_into_edit = match.description;
match.transition = ui::PAGE_TRANSITION_GENERATED;
// Make sure current provider supports image search
TemplateURLService* url_service = client_->GetTemplateURLService();
const TemplateURL* default_url = url_service->GetDefaultSearchProvider();
return match;
}
if (!default_url || default_url->image_url().empty() ||
!default_url->image_url_ref().IsValid(url_service->search_terms_data())) {
return false;
}
void ClipboardProvider::NewClipboardImageMatch(
gfx::Image image,
ClipboardImageMatchCallback callback) {
clipboard_content_->GetRecentImageFromClipboard(base::BindOnce(
&ClipboardProvider::OnReceiveImage,
callback_weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
// We want to get the age here because the contents of the clipboard could
// change after this point. We want the age of the image we actually use, not
// the age of whatever's on the clipboard when the histogram is created (i.e
// when the match is created).
base::TimeDelta clipboard_contents_age =
clipboard_content_->GetClipboardContentAge();
clipboard_content_->GetRecentImageFromClipboard(
base::BindOnce(&ClipboardProvider::OnReceiveImage,
callback_weak_ptr_factory_.GetWeakPtr(), input,
url_service, clipboard_contents_age));
return true;
}
void ClipboardProvider::OnReceiveImage(
ClipboardImageMatchCallback callback,
const AutocompleteInput& input,
TemplateURLService* url_service,
base::TimeDelta clipboard_contents_age,
base::Optional<gfx::Image> optional_image) {
if (!optional_image)
std::move(callback).Run(base::nullopt);
return;
done_ = false;
gfx::ImageSkia image_skia = *optional_image.value().ToImageSkia();
image_skia.MakeThreadSafe();
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE,
base::BindOnce(&ClipboardProvider::EncodeClipboardImage, image_skia),
base::BindOnce(&ClipboardProvider::ConstructImageMatchCallback,
callback_weak_ptr_factory_.GetWeakPtr(),
std::move(callback)));
callback_weak_ptr_factory_.GetWeakPtr(), input,
url_service, clipboard_contents_age));
}
scoped_refptr<base::RefCountedMemory> ClipboardProvider::EncodeClipboardImage(
......@@ -572,13 +396,26 @@ scoped_refptr<base::RefCountedMemory> ClipboardProvider::EncodeClipboardImage(
}
void ClipboardProvider::ConstructImageMatchCallback(
ClipboardImageMatchCallback callback,
const AutocompleteInput& input,
TemplateURLService* url_service,
base::TimeDelta clipboard_contents_age,
scoped_refptr<base::RefCountedMemory> image_bytes) {
TemplateURLService* url_service = client_->GetTemplateURLService();
const TemplateURL* default_url = url_service->GetDefaultSearchProvider();
DCHECK(default_url);
AutocompleteMatch match = NewBlankImageMatch();
AutocompleteMatch match(this, kClipboardMatchRelevanceScore,
IsMatchDeletionEnabled(),
AutocompleteMatchType::CLIPBOARD_IMAGE);
match.description.assign(l10n_util::GetStringUTF16(IDS_IMAGE_FROM_CLIPBOARD));
if (!match.description.empty())
match.description_class.push_back({0, ACMatchClassification::NONE});
// This will end up being something like "Search for Copied Image." This may
// seem strange to use for |fill_into_edit, but it is because iOS requires
// some text in the text field for the Enter key to work when using keyboard
// navigation.
match.fill_into_edit = match.description;
match.search_terms_args =
std::make_unique<TemplateURLRef::SearchTermsArgs>(base::ASCIIToUTF16(""));
......@@ -599,5 +436,19 @@ void ClipboardProvider::ConstructImageMatchCallback(
match.post_content =
std::make_unique<TemplateURLRef::PostContent>(post_content);
std::move(callback).Run(match);
match.transition = ui::PAGE_TRANSITION_GENERATED;
field_trial_triggered_ = true;
field_trial_triggered_in_session_ = true;
done_ = true;
// Some users may be in a counterfactual study arm in which we perform all
// necessary work but do not forward the autocomplete matches.
bool in_counterfactual_group = base::GetFieldTrialParamByFeatureAsBool(
omnibox::kEnableClipboardProviderImageSuggestions,
"ClipboardProviderImageSuggestionsCounterfactualArm", false);
if (!in_counterfactual_group) {
AddCreatedMatchWithTracking(input, match, clipboard_contents_age);
listener_->OnProviderUpdate(true);
}
}
......@@ -12,7 +12,6 @@
class AutocompleteProviderClient;
class ClipboardRecentContent;
class HistoryURLProvider;
enum class ClipboardContentType;
// Autocomplete provider offering content based on the clipboard's content.
class ClipboardProvider : public AutocompleteProvider {
......@@ -25,23 +24,6 @@ class ClipboardProvider : public AutocompleteProvider {
ClipboardProvider(const ClipboardProvider&) = delete;
ClipboardProvider& operator=(const ClipboardProvider&) = delete;
// Returns a new AutocompleteMatch clipboard match that will navigate to the
// given copied url. Used to construct a match later when the URL is not
// available at match creation time (e.g. iOS 14).
AutocompleteMatch NewClipboardURLMatch(GURL url);
// Returns a new AutocompleteMatch clipboard match that will search for the
// given copied text. Used to construct a match later when the text is not
// available at match creation time (e.g. iOS 14).
base::Optional<AutocompleteMatch> NewClipboardTextMatch(base::string16 text);
using ClipboardImageMatchCallback =
base::OnceCallback<void(base::Optional<AutocompleteMatch>)>;
// Returns a new AutocompleteMatch clipboard match that will search for the
// given copied image. Used to construct a match later when the image is not
// available at match creation time (e.g. iOS 14).
void NewClipboardImageMatch(gfx::Image image,
ClipboardImageMatchCallback callback);
// AutocompleteProvider implementation.
void Start(const AutocompleteInput& input, bool minimal_changes) override;
void Stop(bool clear_cached_results, bool due_to_user_inactivity) override;
......@@ -61,72 +43,23 @@ class ClipboardProvider : public AutocompleteProvider {
const AutocompleteMatch& match,
const base::TimeDelta clipboard_contents_age);
// Uses asynchronous clipboard APIs to check which content types have
// clipboard data without actually accessing the data. If any do, then one
// clipboard match is created. Calls back to |OnReceiveClipboardContent| with
// the result.
void CheckClipboardContent(const AutocompleteInput& input);
// Called when the clipboard data is returned from the asynchronous call.
void OnReceiveClipboardContent(const AutocompleteInput& input,
base::TimeDelta clipboard_contents_age,
std::set<ClipboardContentType> matched_types);
// Checks whether the current template url supports text searches.
bool TemplateURLSupportsTextSearch();
// Checks whether the current template url supports image searches.
bool TemplateURLSupportsImageSearch();
// Returns a URL match with no URL. This can be used if the clipboard content
// is inaccessible at match creation time (e.g. iOS 14).
AutocompleteMatch NewBlankURLMatch();
// Returns a text match with no text. This can be used if the clipboard
// content is inaccessible at match creation time (e.g. iOS 14).
AutocompleteMatch NewBlankTextMatch();
// Returns a image match with no attached image. This can be used if the
// clipboard content is inaccessible at match creation time (e.g. iOS 14).
AutocompleteMatch NewBlankImageMatch();
// If there is a url copied to the clipboard and accessing it will not show a
// clipboard access notification (e.g. iOS 14), use it to create a match.
// |read_clipboard_content| will be filled with false if the clipboard didn't
// have any content (either because there was none or because accessing it
// would have shown a clipboard access notification, and true if there was
// content.
// If there is a url copied to the clipboard, use it to create a match.
base::Optional<AutocompleteMatch> CreateURLMatch(
const AutocompleteInput& input,
bool* read_clipboard_content);
// If there is text copied to the clipboard and accessing it will not show a
// clipboard access notification (e.g. iOS 14), use it to create a match.
// |read_clipboard_content| will be filled with false if the clipboard didn't
// have any content (either because there was none or because accessing it
// would have shown a clipboard access notification, and true if there was
// content.
const AutocompleteInput& input);
// If there is text copied to the clipboard, use it to create a match.
base::Optional<AutocompleteMatch> CreateTextMatch(
const AutocompleteInput& input,
bool* read_clipboard_content);
// If there is an image copied to the clipboard and accessing it will not show
// a clipboard access notification (e.g. iOS 14), use it to create a match.
const AutocompleteInput& input);
// If there is an image copied to the clipboard, use it to create a match.
// The image match is asynchronous (because constructing the image post data
// takes time), so instead of returning an optional match like the other
// Create functions, it returns a boolean indicating whether there will be a
// match.
bool CreateImageMatch(const AutocompleteInput& input);
// Handles the callback response from |CreateImageMatch| and turns the image
// into an AutocompleteMatch.
void CreateImageMatchCallback(const AutocompleteInput& input,
const base::TimeDelta clipboard_contents_age,
base::Optional<gfx::Image>);
// Handles the callback response from |CreateImageMatchCallback| and adds the
// created AutocompleteMatch to the matches list.
void AddImageMatchCallback(const AutocompleteInput& input,
const base::TimeDelta clipboard_contents_age,
base::Optional<AutocompleteMatch> match);
// Called when image data is received from clipboard.
void OnReceiveImage(ClipboardImageMatchCallback callback,
// Called when received image data from clipboard.
void OnReceiveImage(const AutocompleteInput& input,
TemplateURLService* url_service,
base::TimeDelta clipboard_contents_age,
base::Optional<gfx::Image> optional_image);
// Resize and encode the image data into bytes. This can take some time if the
......@@ -136,7 +69,9 @@ class ClipboardProvider : public AutocompleteProvider {
// Construct the actual image match once the image has been encoded into
// bytes. This should be called back on the main thread.
void ConstructImageMatchCallback(
ClipboardImageMatchCallback callback,
const AutocompleteInput& input,
TemplateURLService* url_service,
base::TimeDelta clipboard_contents_age,
scoped_refptr<base::RefCountedMemory> image_bytes);
AutocompleteProviderClient* client_;
......
......@@ -82,10 +82,6 @@ class ClipboardProviderTest : public testing::Test,
return input;
}
void MatchesImageCallback(base::Optional<AutocompleteMatch> match) {
matches_image_match_ = match;
}
protected:
// AutocompleteProviderListener:
void OnProviderUpdate(bool updated_matches) override;
......@@ -94,8 +90,6 @@ class ClipboardProviderTest : public testing::Test,
FakeClipboardRecentContent clipboard_content_;
std::unique_ptr<MockAutocompleteProviderClient> client_;
scoped_refptr<ClipboardProvider> provider_;
base::Optional<AutocompleteMatch> matches_image_match_;
};
void ClipboardProviderTest::OnProviderUpdate(bool updated_matches) {
......@@ -108,18 +102,12 @@ TEST_F(ClipboardProviderTest, NotFromOmniboxFocus) {
}
TEST_F(ClipboardProviderTest, EmptyClipboard) {
auto template_url_service = std::make_unique<TemplateURLService>(
/*initializers=*/nullptr, /*count=*/0);
client_->set_template_url_service(std::move(template_url_service));
ClearClipboard();
provider_->Start(CreateAutocompleteInput(OmniboxFocusType::ON_FOCUS), false);
EXPECT_TRUE(provider_->matches().empty());
}
TEST_F(ClipboardProviderTest, ClipboardIsCurrentURL) {
auto template_url_service = std::make_unique<TemplateURLService>(
/*initializers=*/nullptr, /*count=*/0);
client_->set_template_url_service(std::move(template_url_service));
SetClipboardUrl(GURL(kCurrentURL));
provider_->Start(CreateAutocompleteInput(OmniboxFocusType::ON_FOCUS), false);
EXPECT_TRUE(provider_->matches().empty());
......@@ -164,21 +152,20 @@ TEST_F(ClipboardProviderTest, MatchesImage) {
base::Feature imageFeature =
omnibox::kEnableClipboardProviderImageSuggestions;
feature_list.InitAndEnableFeature(imageFeature);
auto template_url_service =
std::make_unique<TemplateURLService>(/*initializers=*/nullptr,
/*count=*/0);
client_->set_template_url_service(std::move(template_url_service));
TemplateURLService template_url_service(/*initializers=*/nullptr,
/*count=*/0);
base::TimeDelta clipboard_age = base::TimeDelta::FromSeconds(5);
gfx::Image test_image = gfx::test::CreateImage(/*height=*/10, /*width=*/10);
scoped_refptr<base::RefCountedMemory> image_bytes =
provider_->EncodeClipboardImage(*test_image.ToImageSkia());
ASSERT_TRUE(image_bytes);
provider_->ConstructImageMatchCallback(
base::BindOnce(&ClipboardProviderTest::MatchesImageCallback,
base::Unretained(this)),
image_bytes);
ASSERT_TRUE(matches_image_match_);
EXPECT_EQ(AutocompleteMatchType::CLIPBOARD_IMAGE, matches_image_match_->type);
CreateAutocompleteInput(OmniboxFocusType::ON_FOCUS),
&template_url_service, clipboard_age, image_bytes);
ASSERT_GE(provider_->matches().size(), 1U);
EXPECT_EQ(AutocompleteMatchType::CLIPBOARD_IMAGE,
provider_->matches().back().type);
}
TEST_F(ClipboardProviderTest, DeleteMatch) {
......
......@@ -33,17 +33,21 @@ class ClipboardRecentContent {
static void SetInstance(std::unique_ptr<ClipboardRecentContent> new_instance);
// Returns clipboard content as URL, if it has a compatible type,
// is recent enough, has not been suppressed and will not trigger a system
// notification that the clipboard has been accessed.
// is recent enough and has not been suppressed.
virtual base::Optional<GURL> GetRecentURLFromClipboard() = 0;
// Returns clipboard content as text, if it has a compatible type,
// is recent enough, has not been suppressed and will not trigger a system
// notification that the clipboard has been accessed.
// is recent enough and has not been suppressed.
virtual base::Optional<base::string16> GetRecentTextFromClipboard() = 0;
// Return if system's clipboard contains an image that will not trigger a
// system notification that the clipboard has been accessed.
using GetRecentImageCallback =
base::OnceCallback<void(base::Optional<gfx::Image>)>;
// Returns clipboard content as image to |GetRecentImageCallback|, if it has a
// compatible type, is recent enough and has not been suppressed.
virtual void GetRecentImageFromClipboard(GetRecentImageCallback callback) = 0;
// Return if system's clipboard contains an image.
virtual bool HasRecentImageFromClipboard() = 0;
/*
......@@ -55,27 +59,19 @@ class ClipboardRecentContent {
using GetRecentURLCallback = base::OnceCallback<void(base::Optional<GURL>)>;
using GetRecentTextCallback =
base::OnceCallback<void(base::Optional<base::string16>)>;
using GetRecentImageCallback =
base::OnceCallback<void(base::Optional<gfx::Image>)>;
// Returns whether the clipboard contains a URL to |HasDataCallback| if it
// is recent enough and has not been suppressed.
virtual void HasRecentContentFromClipboard(
std::set<ClipboardContentType> types,
HasDataCallback callback) = 0;
// Returns clipboard content as URL to |GetRecentURLCallback|, if it has a
// compatible type, is recent enough and has not been suppressed.
virtual void GetRecentURLFromClipboard(GetRecentURLCallback callback) = 0;
// Returns clipboard content as a string to |GetRecentTextCallback|, if it has
// a compatible type, is recent enough and has not been suppressed.
virtual void GetRecentTextFromClipboard(GetRecentTextCallback callback) = 0;
// Returns clipboard content as image to |GetRecentImageCallback|, if it has a
// compatible type, is recent enough and has not been suppressed.
virtual void GetRecentImageFromClipboard(GetRecentImageCallback callback) = 0;
// Returns how old the content of the clipboard is.
virtual base::TimeDelta GetClipboardContentAge() const = 0;
......
......@@ -39,18 +39,15 @@ extern ContentType const ContentTypeImage;
- (instancetype)init NS_UNAVAILABLE;
// Returns the copied URL if the clipboard contains a recent URL that has not
// been suppressed and will not trigger a pasteboard access notification.
// Otherwise, returns nil.
// been supressed. Otherwise, returns nil.
- (NSURL*)recentURLFromClipboard;
// Returns the copied string if the clipboard contains a recent string that has
// not been suppresed and will not trigger a pasteboard access notification.
// Otherwise, returns nil.
// not been suppresed. Otherwise, returns nil.
- (NSString*)recentTextFromClipboard;
// Returns the copied image if the clipboard contains a recent image that has
// not been suppressed and will not trigger a pasteboard access notification.
// Otherwise, returns nil.
// not been suppressed. Otherwise, returns nil.
- (UIImage*)recentImageFromClipboard;
// Uses the new iOS 14 pasteboard detection pattern API to asynchronously detect
......
......@@ -131,13 +131,8 @@ NSString* const kPasteboardChangeDateKey = @"PasteboardChangeDate";
if (![self shouldReturnValueOfClipboard])
return nil;
if (@available(iOS 14, *)) {
// On iOS 14, don't actually access the pasteboard in this method. This
// prevents the pasteboard access notification from appearing.
} else {
if (!self.cachedURL) {
self.cachedURL = [self URLFromPasteboard];
}
if (!self.cachedURL) {
self.cachedURL = [self URLFromPasteboard];
}
return self.cachedURL;
}
......@@ -148,13 +143,8 @@ NSString* const kPasteboardChangeDateKey = @"PasteboardChangeDate";
if (![self shouldReturnValueOfClipboard])
return nil;
if (@available(iOS 14, *)) {
// On iOS 14, don't actually access the pasteboard in this method. This
// prevents the pasteboard access notification from appearing.
} else {
if (!self.cachedText) {
self.cachedText = UIPasteboard.generalPasteboard.string;
}
if (!self.cachedText) {
self.cachedText = UIPasteboard.generalPasteboard.string;
}
return self.cachedText;
}
......@@ -165,13 +155,8 @@ NSString* const kPasteboardChangeDateKey = @"PasteboardChangeDate";
if (![self shouldReturnValueOfClipboard])
return nil;
if (@available(iOS 14, *)) {
// On iOS 14, don't actually access the pasteboard in this method. This
// prevents the pasteboard access notification from appearing.
} else {
if (!self.cachedImage) {
self.cachedImage = UIPasteboard.generalPasteboard.image;
}
if (!self.cachedImage) {
self.cachedImage = UIPasteboard.generalPasteboard.image;
}
return self.cachedImage;
......
......@@ -14,7 +14,6 @@
#include "base/stl_util.h"
#include "base/strings/sys_string_conversions.h"
#include "base/system/sys_info.h"
#include "base/threading/sequenced_task_runner_handle.h"
#import "components/open_from_clipboard/clipboard_recent_content_impl_ios.h"
#import "net/base/mac/url_conversions.h"
#include "url/gurl.h"
......@@ -117,6 +116,19 @@ ClipboardRecentContentIOS::GetRecentTextFromClipboard() {
return base::SysNSStringToUTF16(text_from_pasteboard);
}
void ClipboardRecentContentIOS::GetRecentImageFromClipboard(
GetRecentImageCallback callback) {
__block GetRecentImageCallback callback_for_block = std::move(callback);
[implementation_ recentImageFromClipboardAsync:^(UIImage* image) {
if (!image) {
std::move(callback_for_block).Run(base::nullopt);
return;
}
std::move(callback_for_block).Run(gfx::Image(image));
}];
}
bool ClipboardRecentContentIOS::HasRecentImageFromClipboard() {
return GetRecentImageFromClipboardInternal().has_value();
}
......@@ -129,93 +141,39 @@ void ClipboardRecentContentIOS::HasRecentContentFromClipboard(
for (ClipboardContentType type : types) {
[ios_types addObject:ContentTypeFromClipboardContentType(type)];
}
// The iOS methods for checking clipboard content call their callbacks on an
// arbitrary thread. As Objective-C doesn't have very good thread-management
// techniques, make sure this method calls its callback on the same thread
// that it was called on.
scoped_refptr<base::SequencedTaskRunner> task_runner =
base::SequencedTaskRunnerHandle::Get();
[implementation_
hasContentMatchingTypes:ios_types
completionHandler:^(NSSet<ContentType>* results) {
std::set<ClipboardContentType> matching_types;
for (ContentType type in results) {
matching_types.insert(
ClipboardContentTypeFromContentType(type));
}
task_runner->PostTask(
FROM_HERE, base::BindOnce(^{
std::move(callback_for_block).Run(matching_types);
}));
}];
[implementation_ hasContentMatchingTypes:ios_types
completionHandler:^(NSSet<ContentType>* results) {
std::set<ClipboardContentType> matching_types;
for (ContentType type in results) {
matching_types.insert(
ClipboardContentTypeFromContentType(type));
}
std::move(callback_for_block).Run(matching_types);
}];
}
void ClipboardRecentContentIOS::GetRecentURLFromClipboard(
GetRecentURLCallback callback) {
__block GetRecentURLCallback callback_for_block = std::move(callback);
// The iOS methods for checking clipboard content call their callbacks on an
// arbitrary thread. As Objective-C doesn't have very good thread-management
// techniques, make sure this method calls its callback on the same thread
// that it was called on.
scoped_refptr<base::SequencedTaskRunner> task_runner =
base::SequencedTaskRunnerHandle::Get();
[implementation_ recentURLFromClipboardAsync:^(NSURL* url) {
GURL converted_url = net::GURLWithNSURL(url);
if (!converted_url.is_valid()) {
task_runner->PostTask(FROM_HERE, base::BindOnce(^{
std::move(callback_for_block).Run(base::nullopt);
}));
std::move(callback_for_block).Run(base::nullopt);
return;
}
task_runner->PostTask(FROM_HERE, base::BindOnce(^{
std::move(callback_for_block).Run(converted_url);
}));
std::move(callback_for_block).Run(converted_url);
}];
}
void ClipboardRecentContentIOS::GetRecentTextFromClipboard(
GetRecentTextCallback callback) {
__block GetRecentTextCallback callback_for_block = std::move(callback);
// The iOS methods for checking clipboard content call their callbacks on an
// arbitrary thread. As Objective-C doesn't have very good thread-management
// techniques, make sure this method calls its callback on the same thread
// that it was called on.
scoped_refptr<base::SequencedTaskRunner> task_runner =
base::SequencedTaskRunnerHandle::Get();
[implementation_ recentTextFromClipboardAsync:^(NSString* text) {
if (!text) {
task_runner->PostTask(FROM_HERE, base::BindOnce(^{
std::move(callback_for_block).Run(base::nullopt);
}));
return;
}
task_runner->PostTask(
FROM_HERE, base::BindOnce(^{
std::move(callback_for_block).Run(base::SysNSStringToUTF16(text));
}));
}];
}
void ClipboardRecentContentIOS::GetRecentImageFromClipboard(
GetRecentImageCallback callback) {
__block GetRecentImageCallback callback_for_block = std::move(callback);
// The iOS methods for checking clipboard content call their callbacks on an
// arbitrary thread. As Objective-C doesn't have very good thread-management
// techniques, make sure this method calls its callback on the same thread
// that it was called on.
scoped_refptr<base::SequencedTaskRunner> task_runner =
base::SequencedTaskRunnerHandle::Get();
[implementation_ recentImageFromClipboardAsync:^(UIImage* image) {
if (!image) {
task_runner->PostTask(FROM_HERE, base::BindOnce(^{
std::move(callback_for_block).Run(base::nullopt);
}));
std::move(callback_for_block).Run(base::nullopt);
return;
}
task_runner->PostTask(
FROM_HERE, base::BindOnce(^{
std::move(callback_for_block).Run(gfx::Image(image));
}));
std::move(callback_for_block).Run(base::SysNSStringToUTF16(text));
}];
}
......
......@@ -39,8 +39,6 @@ class OmniboxViewIOS : public OmniboxView,
ChromeBrowserState* browser_state,
id<OmniboxCommands> omnibox_focuser);
~OmniboxViewIOS() override;
void SetPopupProvider(OmniboxPopupProvider* provider) {
popup_provider_ = provider;
}
......@@ -51,41 +49,6 @@ class OmniboxViewIOS : public OmniboxView,
security_state::SecurityLevel security_level,
bool in_dark_mode);
void OnReceiveClipboardURLForOpenMatch(
const AutocompleteMatch& match,
WindowOpenDisposition disposition,
const GURL& alternate_nav_url,
const base::string16& pasted_text,
size_t selected_line,
base::TimeTicks match_selection_timestamp,
base::Optional<GURL> optional_gurl);
void OnReceiveClipboardTextForOpenMatch(
const AutocompleteMatch& match,
WindowOpenDisposition disposition,
const GURL& alternate_nav_url,
const base::string16& pasted_text,
size_t selected_line,
base::TimeTicks match_selection_timestamp,
base::Optional<base::string16> optional_text);
void OnReceiveClipboardImageForOpenMatch(
const AutocompleteMatch& match,
WindowOpenDisposition disposition,
const GURL& alternate_nav_url,
const base::string16& pasted_text,
size_t selected_line,
base::TimeTicks match_selection_timestamp,
base::Optional<gfx::Image> optional_image);
void OnReceiveImageMatchForOpenMatch(
WindowOpenDisposition disposition,
const GURL& alternate_nav_url,
const base::string16& pasted_text,
size_t selected_line,
base::TimeTicks match_selection_timestamp,
base::Optional<AutocompleteMatch> optional_match);
// OmniboxView implementation.
void OpenMatch(const AutocompleteMatch& match,
WindowOpenDisposition disposition,
......@@ -236,9 +199,6 @@ class OmniboxViewIOS : public OmniboxView,
NSMutableAttributedString* attributing_display_string_;
OmniboxPopupProvider* popup_provider_; // weak
// Used to cancel clipboard callbacks if this is deallocated;
base::WeakPtrFactory<OmniboxViewIOS> weak_ptr_factory_{this};
};
#endif // IOS_CHROME_BROWSER_UI_OMNIBOX_OMNIBOX_VIEW_IOS_H_
......@@ -17,12 +17,10 @@
#include "base/strings/sys_string_conversions.h"
#include "components/omnibox/browser/autocomplete_input.h"
#include "components/omnibox/browser/autocomplete_match.h"
#include "components/omnibox/browser/clipboard_provider.h"
#include "components/omnibox/browser/location_bar_model.h"
#include "components/omnibox/browser/omnibox_edit_model.h"
#include "components/omnibox/browser/omnibox_popup_model.h"
#include "components/omnibox/common/omnibox_focus_state.h"
#include "components/open_from_clipboard/clipboard_recent_content.h"
#include "ios/chrome/browser/autocomplete/autocomplete_scheme_classifier_impl.h"
#include "ios/chrome/browser/browser_state/chrome_browser_state.h"
#import "ios/chrome/browser/ui/commands/omnibox_commands.h"
......@@ -97,8 +95,6 @@ OmniboxViewIOS::OmniboxViewIOS(OmniboxTextFieldIOS* field,
!base::ios::IsRunningOnOrLater(11, 2, 0);
}
OmniboxViewIOS::~OmniboxViewIOS() = default;
void OmniboxViewIOS::OpenMatch(const AutocompleteMatch& match,
WindowOpenDisposition disposition,
const GURL& alternate_nav_url,
......@@ -110,125 +106,10 @@ void OmniboxViewIOS::OpenMatch(const AutocompleteMatch& match,
return;
}
// Fill in clipboard matches if they don't have a destination URL.
if (match.destination_url.is_empty()) {
if (match.type == AutocompleteMatchType::CLIPBOARD_URL) {
ClipboardRecentContent* clipboard_recent_content =
ClipboardRecentContent::GetInstance();
clipboard_recent_content->GetRecentURLFromClipboard(base::BindOnce(
&OmniboxViewIOS::OnReceiveClipboardURLForOpenMatch,
weak_ptr_factory_.GetWeakPtr(), match, disposition, alternate_nav_url,
pasted_text, selected_line, match_selection_timestamp));
return;
} else if (match.type == AutocompleteMatchType::CLIPBOARD_TEXT) {
ClipboardRecentContent* clipboard_recent_content =
ClipboardRecentContent::GetInstance();
clipboard_recent_content->GetRecentTextFromClipboard(base::BindOnce(
&OmniboxViewIOS::OnReceiveClipboardTextForOpenMatch,
weak_ptr_factory_.GetWeakPtr(), match, disposition, alternate_nav_url,
pasted_text, selected_line, match_selection_timestamp));
return;
} else if (match.type == AutocompleteMatchType::CLIPBOARD_IMAGE) {
ClipboardRecentContent* clipboard_recent_content =
ClipboardRecentContent::GetInstance();
clipboard_recent_content->GetRecentImageFromClipboard(base::BindOnce(
&OmniboxViewIOS::OnReceiveClipboardImageForOpenMatch,
weak_ptr_factory_.GetWeakPtr(), match, disposition, alternate_nav_url,
pasted_text, selected_line, match_selection_timestamp));
return;
}
}
OmniboxView::OpenMatch(match, disposition, alternate_nav_url, pasted_text,
selected_line, match_selection_timestamp);
}
void OmniboxViewIOS::OnReceiveClipboardURLForOpenMatch(
const AutocompleteMatch& match,
WindowOpenDisposition disposition,
const GURL& alternate_nav_url,
const base::string16& pasted_text,
size_t selected_line,
base::TimeTicks match_selection_timestamp,
base::Optional<GURL> optional_gurl) {
if (!optional_gurl) {
return;
}
GURL url = std::move(optional_gurl).value();
ClipboardProvider* clipboard_provider =
model()->autocomplete_controller()->clipboard_provider();
AutocompleteMatch new_match = clipboard_provider->NewClipboardURLMatch(url);
OmniboxView::OpenMatch(new_match, disposition, alternate_nav_url, pasted_text,
selected_line, match_selection_timestamp);
}
void OmniboxViewIOS::OnReceiveClipboardTextForOpenMatch(
const AutocompleteMatch& match,
WindowOpenDisposition disposition,
const GURL& alternate_nav_url,
const base::string16& pasted_text,
size_t selected_line,
base::TimeTicks match_selection_timestamp,
base::Optional<base::string16> optional_text) {
if (!optional_text) {
return;
}
base::string16 text = std::move(optional_text).value();
ClipboardProvider* clipboard_provider =
model()->autocomplete_controller()->clipboard_provider();
base::Optional<AutocompleteMatch> new_match =
clipboard_provider->NewClipboardTextMatch(text);
if (!new_match) {
return;
}
OmniboxView::OpenMatch(new_match.value(), disposition, alternate_nav_url,
pasted_text, selected_line, match_selection_timestamp);
}
void OmniboxViewIOS::OnReceiveClipboardImageForOpenMatch(
const AutocompleteMatch& match,
WindowOpenDisposition disposition,
const GURL& alternate_nav_url,
const base::string16& pasted_text,
size_t selected_line,
base::TimeTicks match_selection_timestamp,
base::Optional<gfx::Image> optional_image) {
if (!optional_image) {
return;
}
gfx::Image image = std::move(optional_image).value();
ClipboardProvider* clipboard_provider =
model()->autocomplete_controller()->clipboard_provider();
clipboard_provider->NewClipboardImageMatch(
image, base::BindOnce(&OmniboxViewIOS::OnReceiveImageMatchForOpenMatch,
weak_ptr_factory_.GetWeakPtr(), disposition,
alternate_nav_url, pasted_text, selected_line,
match_selection_timestamp));
}
void OmniboxViewIOS::OnReceiveImageMatchForOpenMatch(
WindowOpenDisposition disposition,
const GURL& alternate_nav_url,
const base::string16& pasted_text,
size_t selected_line,
base::TimeTicks match_selection_timestamp,
base::Optional<AutocompleteMatch> optional_match) {
if (!optional_match) {
return;
}
OmniboxView::OpenMatch(optional_match.value(), disposition, alternate_nav_url,
pasted_text, selected_line, match_selection_timestamp);
}
base::string16 OmniboxViewIOS::GetText() const {
return base::SysNSStringToUTF16([field_ displayedText]);
}
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment