Commit 4b6c283c authored by Jeroen Dhollander's avatar Jeroen Dhollander Committed by Commit Bot

Implement BloomServerProxy

This component handles all communication with the Bloom servers.

Bug: b/165356952
Test: chromeos_components_unittests "Bloom*.*"
Change-Id: Ib32a70a466d9024c62ab01d892c9eb381b19afad
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2375994
Commit-Queue: Jeroen Dhollander <jeroendh@chromium.org>
Reviewed-by: default avatarXiaohui Chen <xiaohuic@chromium.org>
Cr-Commit-Position: refs/heads/master@{#809026}
parent 49d0ad67
......@@ -92,7 +92,7 @@ void AssistantClientImpl::MaybeInit(Profile* profile) {
if (chromeos::assistant::features::IsBloomEnabled()) {
bloom_controller_ = chromeos::bloom::BloomControllerFactory::Create(
profile->GetURLLoaderFactory(),
profile->GetURLLoaderFactory()->Clone(),
IdentityManagerFactory::GetForProfile(profile),
nullptr // TODO(jeroendh): pass in real BloomScreenshotDelegate
);
......
......@@ -11,9 +11,12 @@ source_set("bloom") {
"bloom_interaction_observer.h",
"bloom_interaction_observer_impl.cc",
"bloom_interaction_observer_impl.h",
"bloom_server_proxy.h",
"bloom_server_proxy_impl.cc",
"bloom_server_proxy_impl.h",
"server/bloom_server_proxy.h",
"server/bloom_server_proxy_impl.cc",
"server/bloom_server_proxy_impl.h",
"server/bloom_url_loader.h",
"server/bloom_url_loader_impl.cc",
"server/bloom_url_loader_impl.h",
]
deps = [
......@@ -31,7 +34,10 @@ source_set("bloom") {
source_set("unit_tests") {
testonly = true
sources = [ "bloom_controller_impl_unittest.cc" ]
sources = [
"bloom_controller_impl_unittest.cc",
"server/bloom_server_proxy_impl_unittest.cc",
]
deps = [
":bloom",
......@@ -41,6 +47,7 @@ source_set("unit_tests") {
"//chromeos/services/assistant/public/shared",
"//components/signin/public/identity_manager",
"//components/signin/public/identity_manager:test_support",
"//services/network:test_support",
"//services/network/public/cpp",
"//testing/gmock",
"//testing/gtest",
......
......@@ -6,8 +6,8 @@
#include "base/logging.h"
#include "chromeos/components/bloom/bloom_interaction.h"
#include "chromeos/components/bloom/bloom_server_proxy.h"
#include "chromeos/components/bloom/public/cpp/bloom_screenshot_delegate.h"
#include "chromeos/components/bloom/server/bloom_server_proxy.h"
namespace chromeos {
namespace bloom {
......
......@@ -5,9 +5,9 @@
#include "chromeos/components/bloom/bloom_controller_impl.h"
#include "base/test/task_environment.h"
#include "chromeos/components/bloom/bloom_interaction_observer.h"
#include "chromeos/components/bloom/bloom_server_proxy.h"
#include "chromeos/components/bloom/public/cpp/bloom_interaction_resolution.h"
#include "chromeos/components/bloom/public/cpp/bloom_screenshot_delegate.h"
#include "chromeos/components/bloom/server/bloom_server_proxy.h"
#include "chromeos/services/assistant/public/shared/constants.h"
#include "components/signin/public/identity_manager/identity_manager.h"
#include "components/signin/public/identity_manager/identity_test_environment.h"
......
......@@ -8,9 +8,9 @@
#include "base/memory/weak_ptr.h"
#include "chromeos/components/bloom/bloom_controller_impl.h"
#include "chromeos/components/bloom/bloom_interaction.h"
#include "chromeos/components/bloom/bloom_server_proxy.h"
#include "chromeos/components/bloom/public/cpp/bloom_screenshot_delegate.h"
#include "chromeos/components/bloom/public/cpp/future_value.h"
#include "chromeos/components/bloom/server/bloom_server_proxy.h"
#include "chromeos/services/assistant/public/shared/constants.h"
#include "components/signin/public/identity_manager/access_token_info.h"
#include "components/signin/public/identity_manager/primary_account_access_token_fetcher.h"
......@@ -23,7 +23,10 @@ namespace bloom {
BloomInteraction::BloomInteraction(BloomControllerImpl* controller)
: controller_(controller), weak_ptr_factory_(this) {}
BloomInteraction::~BloomInteraction() = default;
BloomInteraction::~BloomInteraction() {
// TODO(jeroendh): Cancel ongoing processes here, like the screenshot fetcher
// and the bloom server requests.
}
void BloomInteraction::Start() {
FetchAccessTokenAsync();
......@@ -39,7 +42,8 @@ void BloomInteraction::StartAssistantInteraction(std::string&& access_token,
controller_->ShowUI();
DVLOG(2) << "Contacting Bloom server";
controller_->server_proxy()->AnalyzeProblem(
access_token, screenshot, Bind(&BloomInteraction::OnServerResponse));
access_token, std::move(screenshot),
Bind(&BloomInteraction::OnServerResponse));
}
void BloomInteraction::OnServerResponse(base::Optional<std::string> html) {
......
......@@ -31,6 +31,7 @@ component("bloom_controller_factory") {
":cpp",
"//base",
"//chromeos/components/bloom",
"//services/network/public/cpp",
]
defines = [ "IS_BLOOM_IMPL" ]
......
......@@ -9,20 +9,23 @@
#include "base/callback.h"
#include "chromeos/components/bloom/bloom_controller_impl.h"
#include "chromeos/components/bloom/bloom_interaction_observer_impl.h"
#include "chromeos/components/bloom/bloom_server_proxy_impl.h"
#include "chromeos/components/bloom/public/cpp/bloom_screenshot_delegate.h"
#include "chromeos/components/bloom/server/bloom_server_proxy_impl.h"
#include "chromeos/components/bloom/server/bloom_url_loader_impl.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
namespace chromeos {
namespace bloom {
// static
std::unique_ptr<BloomController> BloomControllerFactory::Create(
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
std::unique_ptr<network::PendingSharedURLLoaderFactory> url_loader_factory,
signin::IdentityManager* identity_manager,
std::unique_ptr<BloomScreenshotDelegate> screenshot_delegate) {
auto result = std::make_unique<BloomControllerImpl>(
identity_manager, std::move(screenshot_delegate),
std::make_unique<BloomServerProxyImpl>());
std::make_unique<BloomServerProxyImpl>(
std::make_unique<BloomURLLoaderImpl>(std::move(url_loader_factory))));
result->AddObserver(std::make_unique<BloomInteractionObserverImpl>());
......
......@@ -11,7 +11,7 @@
#include "base/memory/scoped_refptr.h"
namespace network {
class SharedURLLoaderFactory;
class PendingSharedURLLoaderFactory;
} // namespace network
namespace signin {
......@@ -28,7 +28,8 @@ class COMPONENT_EXPORT(BLOOM) BloomControllerFactory {
public:
// Create the Bloom controller. Can only be invoked once.
static std::unique_ptr<BloomController> Create(
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
std::unique_ptr<network::PendingSharedURLLoaderFactory>
url_loader_factory,
signin::IdentityManager* identity_manager,
std::unique_ptr<BloomScreenshotDelegate> screenshot_delegate);
};
......
......@@ -2,8 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CHROMEOS_COMPONENTS_BLOOM_BLOOM_SERVER_PROXY_H_
#define CHROMEOS_COMPONENTS_BLOOM_BLOOM_SERVER_PROXY_H_
#ifndef CHROMEOS_COMPONENTS_BLOOM_SERVER_BLOOM_SERVER_PROXY_H_
#define CHROMEOS_COMPONENTS_BLOOM_SERVER_BLOOM_SERVER_PROXY_H_
#include <string>
#include "base/callback.h"
......@@ -36,4 +36,4 @@ class BloomServerProxy {
} // namespace bloom
} // namespace chromeos
#endif // CHROMEOS_COMPONENTS_BLOOM_BLOOM_SERVER_PROXY_H_
#endif // CHROMEOS_COMPONENTS_BLOOM_SERVER_BLOOM_SERVER_PROXY_H_
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chromeos/components/bloom/server/bloom_server_proxy_impl.h"
#include <memory>
#include "base/base64.h"
#include "base/bind.h"
#include "base/check.h"
#include "base/json/json_reader.h"
#include "base/json/json_writer.h"
#include "base/logging.h"
#include "base/memory/weak_ptr.h"
#include "base/values.h"
#include "chromeos/components/bloom/server/bloom_url_loader.h"
#include "chromeos/services/assistant/public/shared/constants.h"
#include "ui/gfx/image/image.h"
namespace chromeos {
namespace bloom {
namespace {
constexpr char kJsonMimeType[] = "application/json";
std::string EncodeImage(const gfx::Image& image) {
auto bytes = image.As1xPNGBytes();
return base::Base64Encode({bytes->front_as<uint8_t>(), bytes->size()});
}
GURL GetUrlWithPath(base::StringPiece path) {
return GURL(assistant::kBloomServiceUrl + path.as_string());
}
GURL GetCreateImageURL() {
return GetUrlWithPath(assistant::kBloomCreateImagePath);
}
GURL GetOcrImageURL(base::StringPiece image_id) {
return GetUrlWithPath(assistant::kBloomOcrImagePath + image_id.as_string());
}
GURL GetSearchProblemURL(base::StringPiece metadata_blob) {
return GetUrlWithPath(assistant::kBloomSearchProblemPath +
metadata_blob.as_string());
}
std::string JSONToString(const base::Value& json) {
std::string result;
bool success = base::JSONWriter::Write(json, &result);
DCHECK(success);
return result;
}
} // namespace
class BloomServerProxyImpl::Worker {
public:
Worker(BloomServerProxyImpl* parent,
const std::string& access_token,
Callback callback)
: parent_(parent),
access_token_(access_token),
callback_(std::move(callback)) {
DCHECK(parent_);
}
~Worker() {
if (callback_)
std::move(callback_).Run(base::nullopt);
}
void AnalyzeProblem(const gfx::Image& screenshot) {
SendCreateImageRequest(screenshot);
}
private:
BloomURLLoader* url_loader() { return parent_->url_loader_.get(); }
void SendCreateImageRequest(const gfx::Image& screenshot) {
DVLOG(3) << "Sending create image request to Bloom servers";
base::Value body{base::Value::Type::DICTIONARY};
body.SetKey("raw_data", base::Value(EncodeImage(screenshot)));
SendPostJSONRequest(GetCreateImageURL(), body,
base::Bind(&Worker::OnCreateImageResponse,
weak_ptr_factory_.GetWeakPtr()));
}
void SendOcrImageRequest(const std::string& image_id) {
DVLOG(3) << "Sending OCR image request to Bloom servers";
SendGetRequest(GetOcrImageURL(image_id),
base::Bind(&Worker::OnOcrImageResponse,
weak_ptr_factory_.GetWeakPtr()));
}
void SendSearchProblemRequest(const std::string& metadata_blob) {
DVLOG(3) << "Sending search problem request to Bloom servers";
SendGetRequest(GetSearchProblemURL(metadata_blob),
base::Bind(&Worker::OnSearchProblemResponse,
weak_ptr_factory_.GetWeakPtr()));
}
void OnCreateImageResponse(base::Optional<std::string> reply) {
if (!reply) {
DVLOG(3) << "Bloom servers responded with ERROR";
FinishWithError();
return;
}
base::Optional<base::Value> json_reply =
base::JSONReader::Read(reply.value());
if (!json_reply) {
LOG(WARNING) << "Bloom servers responded with invalid JSON";
FinishWithError();
return;
}
const std::string* image_id = json_reply.value().FindStringKey("imageId");
if (!image_id) {
LOG(WARNING) << "'imageId' tag is missing from Bloom server response";
FinishWithError();
return;
}
DVLOG(3) << "Bloom servers responded with image_id '" << image_id << "'";
SendOcrImageRequest(*image_id);
}
void OnOcrImageResponse(base::Optional<std::string> reply) {
if (!reply) {
DVLOG(3) << "Bloom servers responded with ERROR";
FinishWithError();
return;
}
base::Optional<base::Value> json_reply =
base::JSONReader::Read(reply.value());
if (!json_reply) {
LOG(WARNING) << "Bloom servers responded with invalid JSON";
FinishWithError();
return;
}
const std::string* metadata_blob =
json_reply.value().FindStringKey("metadataBlob");
if (!metadata_blob) {
LOG(WARNING)
<< "'metadataBlob' tag is missing from Bloom server response.";
FinishWithError();
return;
}
LOG(WARNING) << "Bloom servers responded with valid response";
SendSearchProblemRequest(*metadata_blob);
}
void OnSearchProblemResponse(base::Optional<std::string> reply) {
FinishWithReply(std::move(reply));
}
void SendPostJSONRequest(const GURL& url,
const base::Value& json,
Callback callback) {
url_loader()->SendPostRequest(url, access_token_, JSONToString(json),
kJsonMimeType, std::move(callback));
}
void SendGetRequest(const GURL& url, Callback callback) {
url_loader()->SendGetRequest(url, access_token_, std::move(callback));
}
void FinishWithError() { FinishWithReply(base::nullopt); }
void FinishWithReply(base::Optional<std::string> reply) {
std::move(callback_).Run(std::move(reply));
}
BloomServerProxyImpl* const parent_;
const std::string access_token_;
Callback callback_;
base::WeakPtrFactory<Worker> weak_ptr_factory_{this};
};
BloomServerProxyImpl::BloomServerProxyImpl(
std::unique_ptr<BloomURLLoader> url_loader)
: url_loader_(std::move(url_loader)) {
DCHECK(url_loader_);
}
BloomServerProxyImpl::~BloomServerProxyImpl() = default;
void BloomServerProxyImpl::AnalyzeProblem(const std::string& access_token,
const gfx::Image& screenshot,
Callback callback) {
worker_ = std::make_unique<Worker>(this, access_token, std::move(callback));
worker_->AnalyzeProblem(screenshot);
}
} // namespace bloom
} // namespace chromeos
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CHROMEOS_COMPONENTS_BLOOM_SERVER_BLOOM_SERVER_PROXY_IMPL_H_
#define CHROMEOS_COMPONENTS_BLOOM_SERVER_BLOOM_SERVER_PROXY_IMPL_H_
#include <memory>
#include "chromeos/components/bloom/server/bloom_server_proxy.h"
namespace chromeos {
namespace bloom {
class BloomURLLoader;
class BloomServerProxyImpl : public BloomServerProxy {
public:
explicit BloomServerProxyImpl(std::unique_ptr<BloomURLLoader> url_loader);
~BloomServerProxyImpl() override;
void AnalyzeProblem(const std::string& access_token,
const gfx::Image& screenshot,
Callback callback) override;
BloomURLLoader* url_loader() { return url_loader_.get(); }
private:
class Worker;
void OnWorkerComplete(Worker* worker);
std::unique_ptr<BloomURLLoader> url_loader_;
std::unique_ptr<Worker> worker_;
};
} // namespace bloom
} // namespace chromeos
#endif // CHROMEOS_COMPONENTS_BLOOM_SERVER_BLOOM_SERVER_PROXY_IMPL_H_
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CHROMEOS_COMPONENTS_BLOOM_SERVER_BLOOM_URL_LOADER_H_
#define CHROMEOS_COMPONENTS_BLOOM_SERVER_BLOOM_URL_LOADER_H_
#include <cstdint>
#include <string>
#include "base/callback.h"
#include "base/optional.h"
#include "url/gurl.h"
namespace chromeos {
namespace bloom {
// A wrapper class around |SimpleURLLoader| that allows us to provide fake data
// during unittests.
class BloomURLLoader {
public:
using Callback = base::OnceCallback<void(base::Optional<std::string> reply)>;
virtual ~BloomURLLoader() = default;
virtual void SendPostRequest(const GURL& url,
const std::string& access_token,
std::string&& body,
const std::string& mime_type,
Callback callback) = 0;
virtual void SendGetRequest(const GURL& url,
const std::string& access_token,
Callback callback) = 0;
};
} // namespace bloom
} // namespace chromeos
#endif // CHROMEOS_COMPONENTS_BLOOM_SERVER_BLOOM_URL_LOADER_H_
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chromeos/components/bloom/server/bloom_url_loader_impl.h"
#include "base/logging.h"
#include "base/notreached.h"
#include "base/optional.h"
#include "net/base/net_errors.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/resource_request_body.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "services/network/public/cpp/simple_url_loader.h"
namespace chromeos {
namespace bloom {
namespace {
// TODO(jeroendh) Investigate how big the actual images are (and potentially
// improve compression by using JPEG).
constexpr int kMaxImageSizeInBytes = 5 * 1024 * 1024;
int GetResponseCode(const network::SimpleURLLoader& simple_loader) {
if (simple_loader.ResponseInfo() && simple_loader.ResponseInfo()->headers) {
return simple_loader.ResponseInfo()->headers->response_code();
}
return -1;
}
// Transient stack object that creates and sends a request to the given URL.
// Can safely be deleted at any time.
class HttpRequest {
public:
using Callback = network::SimpleURLLoader::BodyAsStringCallback;
explicit HttpRequest(
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory)
: url_loader_factory_(url_loader_factory),
request_(std::make_unique<network::ResourceRequest>()) {
request()->credentials_mode = network::mojom::CredentialsMode::kOmit;
}
HttpRequest(HttpRequest&) = delete;
HttpRequest& operator=(HttpRequest&) = delete;
~HttpRequest() = default;
HttpRequest& SetURL(const GURL& url) {
request()->url = GURL(url);
return *this;
}
HttpRequest& SetMethod(const char* method) {
request()->method = method;
return *this;
}
HttpRequest& SetAccessToken(const std::string& access_token) {
return AddHeader("Authorization", "Bearer " + access_token);
}
HttpRequest& AddHeader(base::StringPiece name, base::StringPiece value) {
request()->headers.SetHeader(name, value);
return *this;
}
HttpRequest& SetBody(std::string&& body, const std::string& mime_type) {
DCHECK(!request_body_.has_value());
request_body_ = std::move(body);
mime_type_ = mime_type;
return *this;
}
void Send(Callback callback) {
auto simple_loader = network::SimpleURLLoader::Create(
std::move(request_), NO_TRAFFIC_ANNOTATION_YET);
if (request_body_) {
DCHECK(mime_type_);
simple_loader->AttachStringForUpload(request_body_.value(),
mime_type_.value());
}
auto* loader_ptr = simple_loader.get();
loader_ptr->DownloadToString(
url_loader_factory_.get(),
base::BindOnce(HttpRequest::OnDownloadCompleted, std::move(callback),
std::move(simple_loader)),
kMaxImageSizeInBytes);
}
private:
network::ResourceRequest* request() { return request_.get(); }
static void OnDownloadCompleted(
Callback callback,
std::unique_ptr<network::SimpleURLLoader> url_loader,
std::unique_ptr<std::string> response_body) {
if (url_loader->NetError() != net::OK) {
LOG(WARNING) << "Error contacting Bloom server: "
<< net::ErrorToString(url_loader->NetError())
<< ", error code " << GetResponseCode(*url_loader);
}
std::move(callback).Run(std::move(response_body));
}
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory_;
std::unique_ptr<network::ResourceRequest> request_;
base::Optional<std::string> request_body_;
base::Optional<std::string> mime_type_;
};
class PostRequest : public HttpRequest {
public:
explicit PostRequest(
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory)
: HttpRequest(std::move(url_loader_factory)) {
SetMethod(net::HttpRequestHeaders::kPostMethod);
}
};
class GetRequest : public HttpRequest {
public:
explicit GetRequest(
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory)
: HttpRequest(std::move(url_loader_factory)) {
SetMethod(net::HttpRequestHeaders::kGetMethod);
}
};
} // namespace
BloomURLLoaderImpl::BloomURLLoaderImpl(
std::unique_ptr<network::PendingSharedURLLoaderFactory> url_loader_factory)
: url_loader_factory_(network::SharedURLLoaderFactory::Create(
std::move(url_loader_factory))),
weak_ptr_factory_(this) {}
BloomURLLoaderImpl::~BloomURLLoaderImpl() = default;
void BloomURLLoaderImpl::SendPostRequest(const GURL& url,
const std::string& access_token,
std::string&& body,
const std::string& mime_type,
Callback callback) {
PostRequest(url_loader_factory_)
.SetURL(url)
.SetAccessToken(access_token)
.SetBody(std::move(body), mime_type)
.Send(base::BindOnce(&BloomURLLoaderImpl::OnServerResponse,
weak_ptr_factory_.GetWeakPtr(),
std::move(callback)));
}
void BloomURLLoaderImpl::SendGetRequest(const GURL& url,
const std::string& access_token,
Callback callback) {
GetRequest(url_loader_factory_)
.SetURL(url)
.SetAccessToken(access_token)
.Send(base::BindOnce(&BloomURLLoaderImpl::OnServerResponse,
weak_ptr_factory_.GetWeakPtr(),
std::move(callback)));
}
void BloomURLLoaderImpl::OnServerResponse(
Callback callback,
std::unique_ptr<std::string> response_body) {
if (!response_body) {
std::move(callback).Run(base::nullopt);
return;
}
std::move(callback).Run(std::move(*response_body));
}
} // namespace bloom
} // namespace chromeos
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CHROMEOS_COMPONENTS_BLOOM_SERVER_BLOOM_URL_LOADER_IMPL_H_
#define CHROMEOS_COMPONENTS_BLOOM_SERVER_BLOOM_URL_LOADER_IMPL_H_
#include "base/memory/scoped_refptr.h"
#include "base/memory/weak_ptr.h"
#include "chromeos/components/bloom/server/bloom_url_loader.h"
namespace network {
class PendingSharedURLLoaderFactory;
class SharedURLLoaderFactory;
} // namespace network
namespace chromeos {
namespace bloom {
class BloomURLLoaderImpl : public BloomURLLoader {
public:
explicit BloomURLLoaderImpl(
std::unique_ptr<network::PendingSharedURLLoaderFactory>
url_loader_factory);
BloomURLLoaderImpl(BloomURLLoaderImpl&) = delete;
BloomURLLoaderImpl& operator=(BloomURLLoaderImpl&) = delete;
~BloomURLLoaderImpl() override;
void SendPostRequest(const GURL& url,
const std::string& access_token,
std::string&& body,
const std::string& mime_type,
Callback callback) override;
void SendGetRequest(const GURL& url,
const std::string& access_token,
Callback callback) override;
private:
void OnServerResponse(Callback callback,
std::unique_ptr<std::string> response_body);
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory_;
base::WeakPtrFactory<BloomURLLoaderImpl> weak_ptr_factory_;
};
} // namespace bloom
} // namespace chromeos
#endif // CHROMEOS_COMPONENTS_BLOOM_SERVER_BLOOM_URL_LOADER_IMPL_H_
......@@ -37,6 +37,13 @@ extern const char kBloomOcrImagePath[];
COMPONENT_EXPORT(ASSISTANT_SERVICE_SHARED)
extern const char kBloomSearchProblemPath[];
COMPONENT_EXPORT(ASSISTANT_SERVICE_SHARED)
extern const char kBloomCreateImagePath[];
COMPONENT_EXPORT(ASSISTANT_SERVICE_SHARED)
extern const char kBloomOcrImagePath[];
COMPONENT_EXPORT(ASSISTANT_SERVICE_SHARED)
extern const char kBloomSearchProblemPath[];
} // namespace assistant
} // namespace chromeos
......
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