blob: 422f990f725f075f6c7585f24189cd6799ff6153 [file] [log] [blame]
// Copyright 2019 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 "chromecast/browser/webview/web_content_controller.h"
#include <utility>
#include "base/json/json_writer.h"
#include "base/strings/utf_string_conversions.h"
#include "chromecast/base/version.h"
#include "chromecast/browser/cast_web_contents.h"
#include "chromecast/browser/webview/proto/webview.pb.h"
#include "chromecast/browser/webview/webview_input_method_observer.h"
#include "chromecast/browser/webview/webview_navigation_throttle.h"
#include "chromecast/graphics/cast_focus_client_aura.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browsing_data_remover.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/browser/render_widget_host.h"
#include "content/public/browser/render_widget_host_iterator.h"
#include "content/public/browser/render_widget_host_view.h"
#include "content/public/browser/web_contents.h"
#include "third_party/blink/public/common/input/web_touch_event.h"
#include "ui/aura/window.h"
#include "ui/aura/window_delegate.h"
#include "ui/aura/window_tree_host.h"
#include "ui/base/ime/constants.h"
#include "ui/events/event.h"
#include "ui/events/event_constants.h"
#include "ui/events/keycodes/dom/dom_code.h"
#include "ui/events/keycodes/dom/keycode_converter.h"
#include "ui/events/keycodes/keyboard_code_conversion.h"
namespace chromecast {
WebContentController::WebviewWindowVisibilityObserver::
WebviewWindowVisibilityObserver(aura::Window* window,
WebContentController* controller)
: window_(window), controller_(controller) {
DCHECK(window_);
DCHECK(controller_);
window_->AddObserver(this);
}
void WebContentController::WebviewWindowVisibilityObserver::
OnWindowVisibilityChanged(aura::Window* window, bool visible) {
if (window == window_ && visible && window->CanFocus())
controller_->OnVisible(window);
}
void WebContentController::WebviewWindowVisibilityObserver::OnWindowDestroyed(
aura::Window* window) {
if (window == window_)
window_ = nullptr;
}
WebContentController::WebviewWindowVisibilityObserver::
~WebviewWindowVisibilityObserver() {
if (window_)
window_->RemoveObserver(this);
}
WebContentController::WebContentController(Client* client) : client_(client) {
js_channels_ = std::make_unique<WebContentJsChannels>(client_);
JsClientInstance::AddObserver(this);
}
WebContentController::~WebContentController() {
JsClientInstance::RemoveObserver(this);
if (surface_) {
surface_->RemoveSurfaceObserver(this);
surface_->SetEmbeddedSurfaceId(base::RepeatingCallback<viz::SurfaceId()>());
}
if (!current_render_widget_set_.empty()) {
// TODO(b/150955487): A WebContentController can be destructed without us
// having received RenderViewDeleted notifications for all observed
// RenderWidgetHosts, so we go through the current_render_widget_set_ to
// remove the input event observers. It has sometimes been the case (perhaps
// only on a renderer process crash; requires investigation) that an
// observed RenderWidgetHost has disappeared without notification.
// Therefore, it is not safe to call RemoveInputEventObserver on every
// RenderWidgetHost that we started observing; we need to remove only
// from currently live RenderWidgetHosts.
std::unique_ptr<content::RenderWidgetHostIterator> widgets(
content::RenderWidgetHost::GetRenderWidgetHosts());
while (content::RenderWidgetHost* widget = widgets->GetNextHost()) {
auto it = current_render_widget_set_.find(widget);
if (it != current_render_widget_set_.end()) {
widget->RemoveInputEventObserver(this);
}
}
}
}
void WebContentController::ProcessRequest(
const webview::WebviewRequest& request) {
content::WebContents* contents = GetWebContents();
switch (request.type_case()) {
case webview::WebviewRequest::kInput:
ProcessInputEvent(request.input());
break;
case webview::WebviewRequest::kEvaluateJavascript:
if (request.has_evaluate_javascript()) {
HandleEvaluateJavascript(request.id(), request.evaluate_javascript());
} else {
client_->OnError("evaluate_javascript() not supplied");
}
break;
case webview::WebviewRequest::kAddJavascriptChannels:
if (request.has_add_javascript_channels()) {
HandleAddJavascriptChannels(request.add_javascript_channels());
} else {
client_->OnError("add_javascript_channels() not supplied");
}
break;
case webview::WebviewRequest::kRemoveJavascriptChannels:
if (request.has_remove_javascript_channels()) {
HandleRemoveJavascriptChannels(request.remove_javascript_channels());
} else {
client_->OnError("remove_javascript_channels() not supplied");
}
break;
case webview::WebviewRequest::kGetCurrentUrl:
HandleGetCurrentUrl(request.id());
break;
case webview::WebviewRequest::kCanGoBack:
HandleCanGoBack(request.id());
break;
case webview::WebviewRequest::kCanGoForward:
HandleCanGoForward(request.id());
break;
case webview::WebviewRequest::kGoBack:
contents->GetController().GoBack();
break;
case webview::WebviewRequest::kGoForward:
contents->GetController().GoForward();
break;
case webview::WebviewRequest::kReload:
// TODO(dnicoara): Are the default parameters correct?
contents->GetController().Reload(content::ReloadType::NORMAL,
/*check_for_repost=*/true);
break;
case webview::WebviewRequest::kClearCache:
HandleClearCache();
break;
case webview::WebviewRequest::kClearCookies:
HandleClearCookies(request.id());
break;
case webview::WebviewRequest::kGetTitle:
HandleGetTitle(request.id());
break;
case webview::WebviewRequest::kResize:
if (request.has_resize()) {
HandleResize(
gfx::Size(request.resize().width(), request.resize().height()));
} else {
client_->OnError("resize() not supplied");
}
break;
case webview::WebviewRequest::kSetInsets:
if (request.has_set_insets()) {
HandleSetInsets(gfx::Insets(
request.set_insets().top(), request.set_insets().left(),
request.set_insets().bottom(), request.set_insets().right()));
} else {
client_->OnError("set_insets() not supplied");
}
break;
default:
client_->OnError("Unknown request code");
break;
}
}
void WebContentController::AttachTo(aura::Window* window, int window_id) {
// Register our observer on the window so we can act later once it
// becomes visible.
window_visibility_observer_ =
std::make_unique<WebviewWindowVisibilityObserver>(window, this);
content::WebContents* contents = GetWebContents();
auto* contents_window = contents->GetNativeView();
contents_window->set_id(window_id);
// The aura window is hidden to avoid being shown via the usual layer method,
// instead it is shows via a SurfaceDrawQuad by exo.
contents_window->Hide();
window->AddChild(contents_window);
exo::Surface* surface = exo::Surface::AsSurface(window);
CHECK(surface) << "Attaching Webview to non-EXO surface window";
CHECK(!surface_) << "Attaching already attached WebView";
surface_ = surface;
surface_->AddSurfaceObserver(this);
// Unretained is safe because we unset this in the destructor.
surface_->SetEmbeddedSurfaceId(base::BindRepeating(
&WebContentController::GetSurfaceId, base::Unretained(this)));
HandleResize(contents_window->bounds().size());
}
void WebContentController::OnVisible(aura::Window* window) {
// Acquire initial focus.
auto* contents = GetWebContents();
if (contents) contents->SetInitialFocus();
else {
LOG(WARNING)
<< "Webview unable to acquire initial focus due to missing webcontents";
}
// Register for IME events
input_method_observer_ = std::make_unique<WebviewInputMethodObserver>(
this, window->GetHost()->GetInputMethod());
}
void WebContentController::ProcessInputEvent(const webview::InputEvent& ev) {
content::WebContents* contents = GetWebContents();
DCHECK(contents);
// Ensure this web contents has focus before sending it input.
// Focus at this level is necessary, or else Blink will ignore
// attempts to focus any elements in the contents.
//
// Via b/156123509: The aura::Window given by |contents->GetNativeView()|
// is not suitable for this purpose, because it has no OnWindowFocused
// observer. The |window| used here is the same one whose |delegate|
// is the EventHandler for this input event.
content::RenderWidgetHostView* rwhv = contents->GetRenderWidgetHostView();
aura::Window* window = rwhv->GetNativeView();
DCHECK(window == contents->GetContentNativeView());
if (!window->CanFocus())
return;
if (!window->HasFocus())
window->Focus();
ui::EventHandler* handler = rwhv->GetNativeView()->delegate();
ui::EventType type = static_cast<ui::EventType>(ev.event_type());
switch (type) {
case ui::ET_TOUCH_RELEASED:
case ui::ET_TOUCH_PRESSED:
case ui::ET_TOUCH_MOVED:
case ui::ET_TOUCH_CANCELLED:
if (ev.has_touch()) {
auto& touch = ev.touch();
ui::TouchEvent evt(
type, gfx::PointF(touch.x(), touch.y()),
gfx::PointF(touch.root_x(), touch.root_y()),
base::TimeTicks() +
base::TimeDelta::FromMicroseconds(ev.timestamp()),
ui::PointerDetails(
static_cast<ui::EventPointerType>(touch.pointer_type()),
static_cast<ui::PointerId>(touch.pointer_id()),
touch.radius_x(), touch.radius_y(), touch.force(),
touch.twist(), touch.tilt_x(), touch.tilt_y(),
touch.tangential_pressure()),
ev.flags());
ui::TouchEvent root_relative_event(evt);
root_relative_event.set_location_f(evt.root_location_f());
// GestureRecognizerImpl makes several APIs private so cast it to the
// interface.
ui::GestureRecognizer* recognizer = &gesture_recognizer_;
// Run touches through the gesture recognition pipeline, web content
// typically wants to process gesture events, not touch events.
if (!recognizer->ProcessTouchEventPreDispatch(
&root_relative_event, contents->GetNativeView())) {
return;
}
// This flag is set depending on the gestures recognized in the call
// above, and needs to propagate with the forwarded event.
evt.set_may_cause_scrolling(root_relative_event.may_cause_scrolling());
if (type == ui::ET_TOUCH_PRESSED) {
// Ensure that we are observing the RenderWidgetHost for this touch
// sequence, even if we didn't get a WebContentsObserver notification
// for its creation. (This is not the normal case, but can happen
// e.g. when loading a page with the Fling interface.)
RegisterRenderWidgetInputObserver(rwhv->GetRenderWidgetHost());
}
// Record touch event information to match against acks.
TouchData touch_data = {evt.unique_event_id(), rwhv, /*acked*/ false,
/*result*/ ui::ER_UNHANDLED};
touch_queue_.push_back(touch_data);
handler->OnTouchEvent(&evt);
} else {
client_->OnError("touch() not supplied for touch event");
}
break;
case ui::ET_MOUSE_PRESSED:
case ui::ET_MOUSE_DRAGGED:
case ui::ET_MOUSE_RELEASED:
case ui::ET_MOUSE_MOVED:
case ui::ET_MOUSE_ENTERED:
case ui::ET_MOUSE_EXITED:
case ui::ET_MOUSEWHEEL:
case ui::ET_MOUSE_CAPTURE_CHANGED:
if (ev.has_mouse()) {
auto& mouse = ev.mouse();
ui::MouseEvent evt(
type, gfx::PointF(mouse.x(), mouse.y()),
gfx::PointF(mouse.root_x(), mouse.root_y()),
base::TimeTicks() +
base::TimeDelta::FromMicroseconds(ev.timestamp()),
ev.flags(), mouse.changed_button_flags());
if (contents->GetAccessibilityMode().has_mode(
ui::AXMode::kWebContents)) {
evt.set_flags(evt.flags() | ui::EF_TOUCH_ACCESSIBILITY);
}
handler->OnMouseEvent(&evt);
} else {
client_->OnError("mouse() not supplied for mouse event");
}
break;
case ui::ET_KEY_PRESSED:
case ui::ET_KEY_RELEASED:
if (ev.has_key()) {
ui::DomKey dom_key =
ui::KeycodeConverter::KeyStringToDomKey(ev.key().key_string());
// Backspace, delete, and tab have to be treated specially as they are
// characters according to DomKey, but they are non-printable.
bool is_printable_character =
dom_key.IsCharacter() && dom_key != ui::DomKey::TAB &&
dom_key != ui::DomKey::BACKSPACE && dom_key != ui::DomKey::DEL;
ui::KeyboardCode keyboard_code =
is_printable_character
? static_cast<ui::KeyboardCode>(dom_key.ToCharacter())
: NonPrintableDomKeyToKeyboardCode(dom_key);
ui::KeyEvent evt(type, keyboard_code,
UsLayoutKeyboardCodeToDomCode(keyboard_code),
ev.flags() | ui::EF_IS_SYNTHESIZED, dom_key,
base::TimeTicks() +
base::TimeDelta::FromMicroseconds(ev.timestamp()),
is_printable_character);
// Marks the simulated key event is from a Virtual Keyboard.
ui::Event::Properties properties;
properties[ui::kPropertyFromVK] =
std::vector<uint8_t>(ui::kPropertyFromVKSize);
evt.SetProperties(properties);
handler->OnKeyEvent(&evt);
} else {
client_->OnError("key() not supplied for key event");
}
break;
default:
break;
}
}
void WebContentController::RegisterRenderWidgetInputObserverFromRenderFrameHost(
WebContentController* web_content_controller,
content::RenderFrameHost* render_frame_host) {
content::RenderWidgetHostView* view = render_frame_host->GetView();
if (view) {
web_content_controller->RegisterRenderWidgetInputObserver(
view->GetRenderWidgetHost());
}
}
void WebContentController::RegisterRenderWidgetInputObserver(
content::RenderWidgetHost* render_widget_host) {
auto insertion = current_render_widget_set_.insert(render_widget_host);
if (insertion.second) {
render_widget_host->AddInputEventObserver(this);
}
}
void WebContentController::UnregisterRenderWidgetInputObserver(
content::RenderWidgetHost* render_widget_host) {
current_render_widget_set_.erase(render_widget_host);
render_widget_host->RemoveInputEventObserver(this);
}
void WebContentController::JavascriptCallback(int64_t id, base::Value result) {
std::string json;
base::JSONWriter::Write(result, &json);
std::unique_ptr<webview::WebviewResponse> response =
std::make_unique<webview::WebviewResponse>();
response->set_id(id);
response->mutable_evaluate_javascript()->set_json(json);
// Async response may come after Destroy() was called but before the web page
// closed.
if (client_)
client_->EnqueueSend(std::move(response));
}
void WebContentController::HandleEvaluateJavascript(
int64_t id,
const webview::EvaluateJavascriptRequest& request) {
GetWebContents()->GetMainFrame()->ExecuteJavaScript(
base::UTF8ToUTF16(request.javascript_blob()),
base::BindOnce(&WebContentController::JavascriptCallback,
weak_ptr_factory_.GetWeakPtr(), id));
}
void WebContentController::HandleAddJavascriptChannels(
const webview::AddJavascriptChannelsRequest& request) {
for (auto& channel : request.channels()) {
current_javascript_channel_set_.insert(channel);
for (auto* frame : current_render_frame_set_) {
ChannelModified(frame, channel, true);
}
}
}
void WebContentController::HandleRemoveJavascriptChannels(
const webview::RemoveJavascriptChannelsRequest& request) {
for (auto& channel : request.channels()) {
current_javascript_channel_set_.erase(channel);
for (auto* frame : current_render_frame_set_) {
ChannelModified(frame, channel, false);
}
}
}
void WebContentController::HandleGetCurrentUrl(int64_t id) {
std::unique_ptr<webview::WebviewResponse> response =
std::make_unique<webview::WebviewResponse>();
response->set_id(id);
response->mutable_get_current_url()->set_url(
GetWebContents()->GetURL().spec());
client_->EnqueueSend(std::move(response));
}
void WebContentController::HandleCanGoBack(int64_t id) {
std::unique_ptr<webview::WebviewResponse> response =
std::make_unique<webview::WebviewResponse>();
response->set_id(id);
response->mutable_can_go_back()->set_can_go_back(
GetWebContents()->GetController().CanGoBack());
client_->EnqueueSend(std::move(response));
}
void WebContentController::HandleCanGoForward(int64_t id) {
std::unique_ptr<webview::WebviewResponse> response =
std::make_unique<webview::WebviewResponse>();
response->set_id(id);
response->mutable_can_go_forward()->set_can_go_forward(
GetWebContents()->GetController().CanGoForward());
client_->EnqueueSend(std::move(response));
}
void WebContentController::HandleClearCache() {
// TODO(dnicoara): See if there is a generic way to inform the renderer to
// clear cache.
// Android has a specific renderer message for this:
// https://cs.chromium.org/chromium/src/android_webview/common/render_view_messages.h?rcl=65107121555167a3db39de5633c3297f7e861315&l=44
// Remove disk cache and local storage.
content::BrowsingDataRemover* remover =
content::BrowserContext::GetBrowsingDataRemover(
GetWebContents()->GetBrowserContext());
remover->Remove(base::Time(), base::Time::Max(),
content::BrowsingDataRemover::DATA_TYPE_CACHE |
content::BrowsingDataRemover::DATA_TYPE_DOM_STORAGE,
content::BrowsingDataRemover::ORIGIN_TYPE_UNPROTECTED_WEB |
content::BrowsingDataRemover::ORIGIN_TYPE_PROTECTED_WEB);
}
void WebContentController::HandleClearCookies(int64_t id) {
std::unique_ptr<webview::WebviewResponse> response =
std::make_unique<webview::WebviewResponse>();
content::BrowsingDataRemover* remover =
content::BrowserContext::GetBrowsingDataRemover(
GetWebContents()->GetBrowserContext());
remover->Remove(base::Time(), base::Time::Max(),
content::BrowsingDataRemover::DATA_TYPE_COOKIES,
content::BrowsingDataRemover::ORIGIN_TYPE_UNPROTECTED_WEB |
content::BrowsingDataRemover::ORIGIN_TYPE_PROTECTED_WEB);
// There appears to be no way of knowing if this actually clears anything.
response->mutable_clear_cookies()->set_had_cookies(false);
response->set_id(id);
client_->EnqueueSend(std::move(response));
}
void WebContentController::HandleGetTitle(int64_t id) {
std::unique_ptr<webview::WebviewResponse> response =
std::make_unique<webview::WebviewResponse>();
response->set_id(id);
response->mutable_get_title()->set_title(
base::UTF16ToUTF8(GetWebContents()->GetTitle()));
client_->EnqueueSend(std::move(response));
}
void WebContentController::HandleResize(const gfx::Size& size) {
LOG(INFO) << "Sizing web content to " << size.ToString();
GetWebContents()->GetNativeView()->SetBounds(gfx::Rect(size));
if (surface_) {
surface_->SetEmbeddedSurfaceSize(size);
surface_->Commit();
}
}
void WebContentController::HandleSetInsets(const gfx::Insets& insets) {
auto* contents = GetWebContents();
if (contents && contents->GetTopLevelRenderWidgetHostView())
contents->GetTopLevelRenderWidgetHostView()->SetInsets(insets);
}
viz::SurfaceId WebContentController::GetSurfaceId() {
content::WebContents* web_contents = GetWebContents();
// Web contents are destroyed before controller for cast apps.
if (!web_contents)
return viz::SurfaceId();
auto* rwhv = web_contents->GetRenderWidgetHostView();
if (!rwhv)
return viz::SurfaceId();
auto frame_sink_id = rwhv->GetRenderWidgetHost()->GetFrameSinkId();
auto local_surface_id = rwhv->GetNativeView()->GetLocalSurfaceId();
return viz::SurfaceId(frame_sink_id, local_surface_id);
}
void WebContentController::OnSurfaceDestroying(exo::Surface* surface) {
DCHECK_EQ(surface, surface_);
surface->RemoveSurfaceObserver(this);
surface_ = nullptr;
}
void WebContentController::MainFrameWasResized(bool width_changed) {
// The surface ID may have changed, so trigger a new commit to re-issue the
// draw quad.
if (surface_) {
surface_->Commit();
}
}
void WebContentController::FrameSizeChanged(
content::RenderFrameHost* render_frame_host,
const gfx::Size& frame_size) {
// The surface ID may have changed, so trigger a new commit to re-issue the
// draw quad.
if (surface_) {
surface_->Commit();
}
}
void WebContentController::RenderFrameCreated(
content::RenderFrameHost* render_frame_host) {
current_render_frame_set_.insert(render_frame_host);
auto* instance =
JsClientInstance::Find(render_frame_host->GetProcess()->GetID(),
render_frame_host->GetRoutingID());
// If the instance doesn't exist yet the JsClientInstance observer will see
// it later on.
if (instance)
SendInitialChannelSet(instance);
content::RenderWidgetHostView* view = render_frame_host->GetView();
if (view) {
RegisterRenderWidgetInputObserver(view->GetRenderWidgetHost());
}
}
void WebContentController::RenderFrameDeleted(
content::RenderFrameHost* render_frame_host) {
current_render_frame_set_.erase(render_frame_host);
}
void WebContentController::RenderFrameHostChanged(
content::RenderFrameHost* old_host,
content::RenderFrameHost* new_host) {
// The surface ID may have changed, so trigger a new commit to re-issue the
// draw quad.
if (surface_) {
surface_->Commit();
}
}
void WebContentController::RenderViewCreated(
content::RenderViewHost* render_view_host) {
RegisterRenderWidgetInputObserver(render_view_host->GetWidget());
}
void WebContentController::RenderViewDeleted(
content::RenderViewHost* render_view_host) {
content::RenderWidgetHost* rwh = render_view_host->GetWidget();
UnregisterRenderWidgetInputObserver(rwh);
content::RenderWidgetHostView* rwhv = rwh->GetView();
base::EraseIf(touch_queue_,
[rwhv](TouchData data) { return data.rwhv == rwhv; });
}
void WebContentController::OnJsClientInstanceRegistered(
int process_id,
int routing_id,
JsClientInstance* instance) {
if (current_render_frame_set_.find(content::RenderFrameHost::FromID(
process_id, routing_id)) != current_render_frame_set_.end()) {
// If the frame exists in the set then it cannot have been handled by
// RenderFrameCreated.
SendInitialChannelSet(instance);
}
}
void WebContentController::AckTouchEvent(content::RenderWidgetHostView* rwhv,
uint32_t unique_event_id,
ui::EventResult result) {
// GestureRecognizerImpl makes AckTouchEvent private so cast to the interface.
ui::GestureRecognizer* recognizer = &gesture_recognizer_;
auto list = recognizer->AckTouchEvent(unique_event_id, result, false,
rwhv->GetNativeView());
// Forward any resulting gestures.
ui::EventHandler* handler = rwhv->GetNativeView()->delegate();
for (auto& e : list) {
handler->OnGestureEvent(e.get());
}
}
void WebContentController::OnInputEventAck(
blink::mojom::InputEventResultSource source,
blink::mojom::InputEventResultState state,
const blink::WebInputEvent& e) {
if (!blink::WebInputEvent::IsTouchEventType(e.GetType()))
return;
const uint32_t id =
static_cast<const blink::WebTouchEvent&>(e).unique_touch_event_id;
ui::EventResult result =
state == blink::mojom::InputEventResultState::kConsumed
? ui::ER_HANDLED
: ui::ER_UNHANDLED;
const auto it = find_if(touch_queue_.begin(), touch_queue_.end(),
[id](TouchData data) { return data.id == id; });
if (it == touch_queue_.end()) {
content::WebContents* contents = GetWebContents();
content::RenderWidgetHostView* rwhv = contents->GetRenderWidgetHostView();
AckTouchEvent(rwhv, id, result);
} else {
// Record the ack.
it->acked = true;
it->result = result;
// Handle any available acks.
while (!touch_queue_.empty() && touch_queue_.front().acked) {
TouchData data = touch_queue_.front();
touch_queue_.pop_front();
AckTouchEvent(data.rwhv, data.id, data.result);
}
}
}
void WebContentController::ChannelModified(content::RenderFrameHost* frame,
const std::string& channel,
bool added) {
auto* instance = JsClientInstance::Find(frame->GetProcess()->GetID(),
frame->GetRoutingID());
if (instance) {
if (added) {
instance->AddChannel(channel, GetJsChannelCallback());
} else {
instance->RemoveChannel(channel);
}
} else {
LOG(WARNING) << "Cannot change channel " << channel << " for "
<< frame->GetLastCommittedURL().possibly_invalid_spec();
}
}
JsChannelCallback WebContentController::GetJsChannelCallback() {
return base::BindRepeating(&WebContentJsChannels::SendMessage,
js_channels_->AsWeakPtr());
}
void WebContentController::SendInitialChannelSet(JsClientInstance* instance) {
// Calls may come after Destroy() was called but before the web page closed.
if (!js_channels_)
return;
JsChannelCallback callback = GetJsChannelCallback();
for (auto& channel : current_javascript_channel_set_)
instance->AddChannel(channel, callback);
}
WebContentJsChannels::WebContentJsChannels(WebContentController::Client* client)
: client_(client) {}
WebContentJsChannels::~WebContentJsChannels() = default;
void WebContentJsChannels::SendMessage(const std::string& channel,
const std::string& message) {
std::unique_ptr<webview::WebviewResponse> response =
std::make_unique<webview::WebviewResponse>();
auto* js_message = response->mutable_javascript_channel_message();
js_message->set_channel(channel);
js_message->set_message(message);
client_->EnqueueSend(std::move(response));
}
} // namespace chromecast