chromium/chromecast/renderer/cast_window_manager_bindings.cc

// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chromecast/renderer/cast_window_manager_bindings.h"

#include <array>
#include <tuple>

#include "base/check.h"
#include "build/build_config.h"
#include "chromecast/base/cast_features.h"
#include "chromecast/common/feature_constants.h"
#include "chromecast/common/mojom/gesture.mojom.h"
#include "chromecast/renderer/feature_manager.h"
#include "content/public/renderer/render_frame.h"
#include "gin/data_object_builder.h"
#include "third_party/blink/public/platform/browser_interface_broker_proxy.h"
#include "third_party/blink/public/platform/scheduler/web_agent_group_scheduler.h"
#include "third_party/blink/public/web/web_local_frame.h"
#include "third_party/blink/public/web/web_view.h"

namespace chromecast {
namespace shell {

namespace {

const char kBindingsObjectName[] = "windowManager";
const char kOnBackGestureName[] = "onBackGesture";
const char kOnBackGestureProgressName[] = "onBackGestureProgress";
const char kOnBackGestureCancelName[] = "onBackGestureCancel";
const char kOnTopDragGestureDoneName[] = "onTopDragGestureDone";
const char kOnTopDragGestureProgressName[] = "onTopDragGestureProgress";
const char kOnRightDragGestureDoneName[] = "onRightDragGestureDone";
const char kOnRightDragGestureProgressName[] = "onRightDragGestureProgress";
const char kOnTapGestureName[] = "onTapGesture";
const char kOnTapDownGestureName[] = "onTapDownGesture";
const char kCanGoBackName[] = "canGoBack";
const char kCanTopDragName[] = "canTopDrag";
const char kCanRightDragName[] = "canRightDrag";
const char kMinimize[] = "minimize";
const char kMaximize[] = "maximize";

#if !BUILDFLAG(IS_ANDROID)
const char kDisplayControlsName[] = "displayControls";
#endif

void OnGestureSourceDisconnectionError() {
  LOG(ERROR) << "Connection error talking to system gesture source.";
}

void OnWindowDisconnect() {
  LOG(ERROR) << "Connection error talking to platform activity.";
}

}  // namespace

CastWindowManagerBindings::CastWindowManagerBindings(
    content::RenderFrame* render_frame,
    const FeatureManager* feature_manager)
    : CastBinding(render_frame),
      feature_manager_(feature_manager),
      handler_receiver_(this) {
  DCHECK(feature_manager_);
}

CastWindowManagerBindings::~CastWindowManagerBindings() {}

void CastWindowManagerBindings::Install(v8::Local<v8::Object> cast_platform,
                                        v8::Isolate* isolate) {
  if (feature_manager_->FeatureEnabled(feature::kEnableSystemGestures)) {
    v8::Local<v8::Object> windowManagerObject =
        EnsureObjectExists(isolate, cast_platform, kBindingsObjectName);

    // On back bindings.
    InstallBinding(isolate, windowManagerObject, kCanGoBackName,
                   &CastWindowManagerBindings::SetCanGoBack,
                   base::Unretained(this));
    InstallBinding(isolate, windowManagerObject, kOnBackGestureName,
                   &CastWindowManagerBindings::SetV8Callback,
                   base::Unretained(this), &on_back_gesture_callback_);
    InstallBinding(isolate, windowManagerObject, kOnBackGestureProgressName,
                   &CastWindowManagerBindings::SetV8Callback,
                   base::Unretained(this), &on_back_gesture_progress_callback_);
    InstallBinding(isolate, windowManagerObject, kOnBackGestureCancelName,
                   &CastWindowManagerBindings::SetV8Callback,
                   base::Unretained(this), &on_back_gesture_cancel_callback_);

    // Top drag bindings.
    InstallBinding(isolate, windowManagerObject, kCanTopDragName,
                   &CastWindowManagerBindings::SetCanTopDrag,
                   base::Unretained(this));
    InstallBinding(isolate, windowManagerObject, kOnTopDragGestureDoneName,
                   &CastWindowManagerBindings::SetV8Callback,
                   base::Unretained(this), &on_top_drag_gesture_done_callback_);
    InstallBinding(isolate, windowManagerObject, kOnTopDragGestureProgressName,
                   &CastWindowManagerBindings::SetV8Callback,
                   base::Unretained(this),
                   &on_top_drag_gesture_progress_callback_);

    // Right drag bindings.
    InstallBinding(isolate, windowManagerObject, kCanRightDragName,
                   &CastWindowManagerBindings::SetCanRightDrag,
                   base::Unretained(this));
    InstallBinding(isolate, windowManagerObject, kOnRightDragGestureDoneName,
                   &CastWindowManagerBindings::SetV8Callback,
                   base::Unretained(this),
                   &on_right_drag_gesture_done_callback_);
    InstallBinding(
        isolate, windowManagerObject, kOnRightDragGestureProgressName,
        &CastWindowManagerBindings::SetV8Callback, base::Unretained(this),
        &on_right_drag_gesture_progress_callback_);

    // 'Tap' bindings.
    InstallBinding(isolate, windowManagerObject, kOnTapGestureName,
                   &CastWindowManagerBindings::SetV8Callback,
                   base::Unretained(this), &on_tap_gesture_callback_);
    InstallBinding(isolate, windowManagerObject, kOnTapDownGestureName,
                   &CastWindowManagerBindings::SetV8Callback,
                   base::Unretained(this), &on_tap_down_gesture_callback_);
  }
  if (feature_manager_->FeatureEnabled(feature::kEnableWindowControls)) {
    v8::Local<v8::Object> windowManagerObject =
        EnsureObjectExists(isolate, cast_platform, kBindingsObjectName);

    InstallBinding(isolate, windowManagerObject, kMaximize,
                   &CastWindowManagerBindings::Show, base::Unretained(this));
    InstallBinding(isolate, windowManagerObject, kMinimize,
                   &CastWindowManagerBindings::Hide, base::Unretained(this));
  }
}

v8::Local<v8::Value> CastWindowManagerBindings::SetV8Callback(
    v8::UniquePersistent<v8::Function>* callback_function,
    v8::Local<v8::Function> callback) {
  v8::Isolate* isolate =
      render_frame()->GetWebFrame()->GetAgentGroupScheduler()->Isolate();

  *callback_function = v8::UniquePersistent<v8::Function>(isolate, callback);

  return v8::Undefined(isolate);
}

void CastWindowManagerBindings::Show() {
  BindWindow();
  window_->Show();
}

void CastWindowManagerBindings::Hide() {
  BindWindow();
  window_->Hide();
}

void CastWindowManagerBindings::SetCanGoBack(bool can_go_back) {
  BindGestureSource();
  gesture_source_->SetCanGoBack(can_go_back);
}

void CastWindowManagerBindings::SetCanTopDrag(bool can_top_drag) {
  BindGestureSource();
  gesture_source_->SetCanTopDrag(can_top_drag);
}

void CastWindowManagerBindings::SetCanRightDrag(bool can_right_drag) {
  BindGestureSource();
  gesture_source_->SetCanRightDrag(can_right_drag);
}

void CastWindowManagerBindings::OnTouchInputSupportSet(
    PersistedResolver resolver,
    PersistedContext original_context,
    bool resolve_promise,
    bool display_controls) {
  DVLOG(2) << __FUNCTION__;
  v8::Isolate* isolate =
      render_frame()->GetWebFrame()->GetAgentGroupScheduler()->Isolate();
  v8::HandleScope handle_scope(isolate);
  v8::Local<v8::Context> context = original_context.Get(isolate);
  v8::MicrotasksScope microtasks_scope(
      context, v8::MicrotasksScope::kDoNotRunMicrotasks);
  v8::Context::Scope context_scope(context);

  if (resolve_promise) {
    resolver.Get(isolate)
        ->Resolve(context,

#if !BUILDFLAG(IS_ANDROID)
                  gin::DataObjectBuilder(isolate)
                      .Set(kDisplayControlsName, display_controls)
                      .Build()
#else
                  v8::Undefined(isolate)
#endif
                      )
        .ToChecked();
  } else {
    resolver.Get(isolate)->Reject(context, v8::Undefined(isolate)).ToChecked();
  }
}

void CastWindowManagerBindings::BindGestureSource() {
  if (gesture_source_.is_bound() && gesture_source_.is_connected())
    return;
  gesture_source_.reset();
  render_frame()->GetBrowserInterfaceBroker().GetInterface(
      gesture_source_.BindNewPipeAndPassReceiver());
  gesture_source_.set_disconnect_handler(
      base::BindRepeating(&OnGestureSourceDisconnectionError));
  handler_receiver_.reset();
  gesture_source_->Subscribe(handler_receiver_.BindNewPipeAndPassRemote());
}

void CastWindowManagerBindings::BindWindow() {
  if (window_.is_bound() && window_.is_connected())
    return;
  window_.reset();
  render_frame()->GetBrowserInterfaceBroker().GetInterface(
      window_.BindNewPipeAndPassReceiver());
  window_.set_disconnect_handler(base::BindRepeating(&OnWindowDisconnect));
}

void CastWindowManagerBindings::InvokeV8Callback(
    v8::UniquePersistent<v8::Function>* callback_function) {
  if (callback_function->IsEmpty()) {
    return;
  }

  blink::WebLocalFrame* web_frame = render_frame()->GetWebFrame();
  v8::Isolate* isolate = web_frame->GetAgentGroupScheduler()->Isolate();
  v8::HandleScope handle_scope(isolate);
  v8::Local<v8::Context> context = web_frame->MainWorldScriptContext();
  v8::Context::Scope context_scope(context);
  v8::Local<v8::Function> handler =
      v8::Local<v8::Function>::New(isolate, std::move(*callback_function));

  v8::MaybeLocal<v8::Value> maybe_result =
      handler->Call(context, context->Global(), 0, nullptr);

  *callback_function = v8::UniquePersistent<v8::Function>(isolate, handler);

  v8::Local<v8::Value> result;
  std::ignore = maybe_result.ToLocal(&result);
}

void CastWindowManagerBindings::InvokeV8Callback(
    v8::UniquePersistent<v8::Function>* callback_function,
    const gfx::Point& touch_location) {
  if (callback_function->IsEmpty()) {
    return;
  }

  blink::WebLocalFrame* web_frame = render_frame()->GetWebFrame();
  v8::Isolate* isolate = web_frame->GetAgentGroupScheduler()->Isolate();
  v8::HandleScope handle_scope(isolate);
  v8::Local<v8::Context> context = web_frame->MainWorldScriptContext();
  v8::Context::Scope context_scope(context);
  v8::Local<v8::Function> handler =
      v8::Local<v8::Function>::New(isolate, std::move(*callback_function));

  v8::Local<v8::Number> touch_x = v8::Integer::New(isolate, touch_location.x());
  v8::Local<v8::Number> touch_y = v8::Integer::New(isolate, touch_location.y());

  auto args = v8::to_array<v8::Local<v8::Value>>({touch_x, touch_y});

  v8::MaybeLocal<v8::Value> maybe_result =
      handler->Call(context, context->Global(), args.size(), args.data());

  *callback_function = v8::UniquePersistent<v8::Function>(isolate, handler);

  v8::Local<v8::Value> result;
  std::ignore = maybe_result.ToLocal(&result);
}

void CastWindowManagerBindings::OnBackGesture(
    ::chromecast::mojom::GestureHandler::OnBackGestureCallback callback) {
  // Note: Can't use InvokeV8Callback here because of the OnBackGestureCallback
  // argument. So we have this boilerplate here until we can get rid of the
  // unused callback argument.
  if (on_back_gesture_callback_.IsEmpty()) {
    std::move(callback).Run(false);
    return;
  }

  blink::WebLocalFrame* web_frame = render_frame()->GetWebFrame();
  v8::Isolate* isolate = web_frame->GetAgentGroupScheduler()->Isolate();
  v8::HandleScope handle_scope(isolate);
  v8::Local<v8::Context> context = web_frame->MainWorldScriptContext();
  v8::Context::Scope context_scope(context);
  v8::Local<v8::Function> handler = v8::Local<v8::Function>::New(
      isolate, std::move(on_back_gesture_callback_));
  auto result = handler->Call(context, context->Global(), 0, nullptr);

  on_back_gesture_callback_ =
      v8::UniquePersistent<v8::Function>(isolate, handler);
  if (result.IsEmpty()) {
    LOG(ERROR) << "No value from callback execution; ";
    std::move(callback).Run(false);
    return;
  }

  auto callback_return_value = result.ToLocalChecked();
  bool return_boolean = callback_return_value->BooleanValue(isolate);
  std::move(callback).Run(return_boolean);
}

void CastWindowManagerBindings::OnBackGestureProgress(
    const gfx::Point& touch_location) {
  InvokeV8Callback(&on_back_gesture_progress_callback_, touch_location);
}

void CastWindowManagerBindings::OnBackGestureCancel() {
  InvokeV8Callback(&on_back_gesture_cancel_callback_);
}

void CastWindowManagerBindings::OnTopDragGestureDone() {
  InvokeV8Callback(&on_top_drag_gesture_done_callback_);
}

void CastWindowManagerBindings::OnTopDragGestureProgress(
    const gfx::Point& touch_location) {
  InvokeV8Callback(&on_top_drag_gesture_progress_callback_, touch_location);
}

void CastWindowManagerBindings::OnRightDragGestureDone() {
  InvokeV8Callback(&on_right_drag_gesture_done_callback_);
}

void CastWindowManagerBindings::OnRightDragGestureProgress(
    const gfx::Point& touch_location) {
  InvokeV8Callback(&on_right_drag_gesture_progress_callback_, touch_location);
}

void CastWindowManagerBindings::OnTapGesture() {
  InvokeV8Callback(&on_tap_gesture_callback_);
}

void CastWindowManagerBindings::OnTapDownGesture() {
  InvokeV8Callback(&on_tap_down_gesture_callback_);
}

}  // namespace shell
}  // namespace chromecast