chromium/chrome/browser/ui/ash/web_view/ash_web_view_impl.cc

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

#include "chrome/browser/ui/ash/web_view/ash_web_view_impl.h"

#include "ash/public/cpp/window_properties.h"
#include "base/task/sequenced_task_runner.h"
#include "chrome/browser/media/webrtc/media_capture_devices_dispatcher.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "content/public/browser/focused_node_details.h"
#include "content/public/browser/host_zoom_map.h"
#include "content/public/browser/media_session.h"
#include "content/public/browser/page.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/browser/render_widget_host_view.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_delegate.h"
#include "third_party/blink/public/common/renderer_preferences/renderer_preferences.h"
#include "third_party/blink/public/mojom/mediastream/media_stream.mojom.h"
#include "ui/aura/window.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/gfx/geometry/rounded_corners_f.h"
#include "ui/views/controls/webview/web_contents_set_background_color.h"
#include "ui/views/controls/webview/webview.h"
#include "ui/views/focus/focus_manager.h"
#include "ui/views/view.h"
#include "ui/views/widget/widget.h"

namespace {
void FixZoomLevelToOne(content::RenderFrameHost* render_frame_host) {
  content::HostZoomMap* zoom_map =
      content::HostZoomMap::Get(render_frame_host->GetSiteInstance());
  zoom_map->SetTemporaryZoomLevel(render_frame_host->GetGlobalId(), 1.0);
}
}  // namespace

AshWebViewImpl::AshWebViewImpl(const InitParams& params) : params_(params) {
  Profile* profile = ProfileManager::GetActiveUserProfile();
  InitWebContents(profile);
  InitLayout(profile);
}

AshWebViewImpl::~AshWebViewImpl() {
  Observe(nullptr);
  web_contents_->SetDelegate(nullptr);
}

gfx::NativeView AshWebViewImpl::GetNativeView() {
  return web_contents_->GetNativeView();
}

void AshWebViewImpl::ChildPreferredSizeChanged(views::View* child) {
  DCHECK_EQ(web_view_, child);
  SetPreferredSize(web_view_->GetPreferredSize());
}

void AshWebViewImpl::Layout(PassKey) {
  web_view_->SetBoundsRect(GetContentsBounds());
}

void AshWebViewImpl::AddObserver(Observer* observer) {
  observers_.AddObserver(observer);
}

void AshWebViewImpl::RemoveObserver(Observer* observer) {
  observers_.RemoveObserver(observer);
}

bool AshWebViewImpl::GoBack() {
  if (web_contents_->GetController().CanGoBack()) {
    web_contents_->GetController().GoBack();
    return true;
  }
  return false;
}

void AshWebViewImpl::Navigate(const GURL& url) {
  content::NavigationController::LoadURLParams params(url);
  web_contents_->GetController().LoadURLWithParams(params);
}

const GURL& AshWebViewImpl::GetVisibleURL() {
  return web_contents_->GetVisibleURL();
}

bool AshWebViewImpl::IsErrorDocument() {
  return web_contents_->GetPrimaryMainFrame()->IsErrorDocument();
}

views::View* AshWebViewImpl::GetInitiallyFocusedView() {
  return web_view_;
}

void AshWebViewImpl::SetCornerRadii(const gfx::RoundedCornersF& corner_radii) {
  web_view_->holder()->SetCornerRadii(corner_radii);
}

const base::UnguessableToken& AshWebViewImpl::GetMediaSessionRequestId() {
  return content::MediaSession::GetRequestIdFromWebContents(
      web_contents_.get());
}

void AshWebViewImpl::AddedToWidget() {
  UpdateMinimizeOnBackProperty();
  AshWebView::AddedToWidget();

  // Apply rounded corners. This can't be done earlier since it
  // requires `web_view_->holder()->native_view()` to be initialized.
  if (params_.rounded_corners.has_value()) {
    web_view_->holder()->SetCornerRadii(params_.rounded_corners.value());
  }
}

bool AshWebViewImpl::IsWebContentsCreationOverridden(
    content::SiteInstance* source_site_instance,
    content::mojom::WindowContainerType window_container_type,
    const GURL& opener_url,
    const std::string& frame_name,
    const GURL& target_url) {
  if (params_.suppress_navigation) {
    NotifyDidSuppressNavigation(target_url,
                                WindowOpenDisposition::NEW_FOREGROUND_TAB,
                                /*from_user_gesture=*/true);
    return true;
  }
  return content::WebContentsDelegate::IsWebContentsCreationOverridden(
      source_site_instance, window_container_type, opener_url, frame_name,
      target_url);
}

content::WebContents* AshWebViewImpl::OpenURLFromTab(
    content::WebContents* source,
    const content::OpenURLParams& params,
    base::OnceCallback<void(content::NavigationHandle&)>
        navigation_handle_callback) {
  if (params_.suppress_navigation) {
    NotifyDidSuppressNavigation(params.url, params.disposition,
                                params.user_gesture);
    return nullptr;
  }
  return content::WebContentsDelegate::OpenURLFromTab(
      source, params, std::move(navigation_handle_callback));
}

void AshWebViewImpl::ResizeDueToAutoResize(content::WebContents* web_contents,
                                           const gfx::Size& new_size) {
  DCHECK_EQ(web_contents_.get(), web_contents);
  web_view_->SetPreferredSize(new_size);
}

bool AshWebViewImpl::TakeFocus(content::WebContents* web_contents,
                               bool reverse) {
  DCHECK_EQ(web_contents_.get(), web_contents);
  auto* focus_manager = GetFocusManager();
  if (focus_manager) {
    focus_manager->ClearNativeFocus();
  }
  return false;
}

void AshWebViewImpl::NavigationStateChanged(
    content::WebContents* web_contents,
    content::InvalidateTypes changed_flags) {
  DCHECK_EQ(web_contents_.get(), web_contents);
  UpdateCanGoBack();
}

void AshWebViewImpl::RequestMediaAccessPermission(
    content::WebContents* web_contents,
    const content::MediaStreamRequest& request,
    content::MediaResponseCallback callback) {
  if (!params_.can_record_media) {
    std::move(callback).Run(
        blink::mojom::StreamDevicesSet(),
        blink::mojom::MediaStreamRequestResult::NOT_SUPPORTED,
        std::unique_ptr<content::MediaStreamUI>());
    return;
  }
  MediaCaptureDevicesDispatcher::GetInstance()->ProcessMediaAccessRequest(
      web_contents, request, std::move(callback), /*extension=*/nullptr);
}

bool AshWebViewImpl::CheckMediaAccessPermission(
    content::RenderFrameHost* render_frame_host,
    const url::Origin& security_origin,
    blink::mojom::MediaStreamType type) {
  if (!params_.can_record_media) {
    return false;
  }
  return MediaCaptureDevicesDispatcher::GetInstance()
      ->CheckMediaAccessPermission(render_frame_host, security_origin, type);
}

std::string AshWebViewImpl::GetTitleForMediaControls(
    content::WebContents* web_contents) {
  if (!params_.source_title.empty()) {
    return params_.source_title;
  }

  return content::WebContentsDelegate::GetTitleForMediaControls(web_contents);
}

void AshWebViewImpl::DidStopLoading() {
  for (auto& observer : observers_) {
    observer.DidStopLoading();
  }
}

void AshWebViewImpl::OnFocusChangedInPage(
    content::FocusedNodeDetails* details) {
  // When navigating to the |web_contents_|, it may not focus it. Request focus
  // as needed. This is a workaround to get a non-empty rect of the focused
  // node. See details in b/177047240.
  auto* native_view = web_contents_->GetContentNativeView();
  if (native_view && !native_view->HasFocus()) {
    web_contents_->Focus();
  }

  for (auto& observer : observers_) {
    observer.DidChangeFocusedNode(details->node_bounds_in_screen);
  }
}

void AshWebViewImpl::RenderFrameHostChanged(
    content::RenderFrameHost* old_host,
    content::RenderFrameHost* new_host) {
  if (new_host != new_host->GetOutermostMainFrame()) {
    return;
  }
  if (params_.fix_zoom_level_to_one) {
    FixZoomLevelToOne(new_host);
  }
}

void AshWebViewImpl::PrimaryPageChanged(content::Page& page) {
  DCHECK_EQ(&page.GetMainDocument(), web_contents_->GetPrimaryMainFrame());
  if (!web_contents_->GetRenderWidgetHostView()) {
    return;
  }

  if (!params_.enable_auto_resize) {
    return;
  }

  gfx::Size min_size(1, 1);
  if (params_.min_size) {
    min_size.SetToMax(params_.min_size.value());
  }

  gfx::Size max_size(INT_MAX, INT_MAX);
  if (params_.max_size) {
    max_size.SetToMin(params_.max_size.value());
  }

  web_contents_->GetRenderWidgetHostView()->EnableAutoResize(min_size,
                                                             max_size);
}

void AshWebViewImpl::NavigationEntriesDeleted() {
  UpdateCanGoBack();
}

void AshWebViewImpl::InitWebContents(Profile* profile) {
  auto web_contents_params = content::WebContents::CreateParams(
      profile, content::SiteInstance::Create(profile));
  web_contents_params.enable_wake_locks = params_.enable_wake_locks;
  web_contents_ = content::WebContents::Create(web_contents_params);

  web_contents_->SetDelegate(this);
  Observe(web_contents_.get());

  // Use a transparent background.
  views::WebContentsSetBackgroundColor::CreateForWebContentsWithColor(
      web_contents_.get(), SK_ColorTRANSPARENT);

  // If requested, suppress navigation.
  if (params_.suppress_navigation) {
    web_contents_->GetMutableRendererPrefs()
        ->browser_handles_all_top_level_requests = true;
    web_contents_->SyncRendererPrefs();
  }

  if (params_.fix_zoom_level_to_one) {
    FixZoomLevelToOne(web_contents_->GetPrimaryMainFrame());
  }
}

void AshWebViewImpl::InitLayout(Profile* profile) {
  web_view_ = AddChildView(std::make_unique<views::WebView>(profile));
  web_view_->SetID(ash::kAshWebViewChildWebViewId);
  web_view_->SetWebContents(web_contents_.get());
}

void AshWebViewImpl::NotifyDidSuppressNavigation(
    const GURL& url,
    WindowOpenDisposition disposition,
    bool from_user_gesture) {
  // Note that we post notification to |observers_| as an observer may cause
  // |this| to be deleted during handling of the event which is unsafe to do
  // until the original navigation sequence has been completed.
  base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE,
      base::BindOnce(
          [](const base::WeakPtr<AshWebViewImpl>& self, GURL url,
             WindowOpenDisposition disposition, bool from_user_gesture) {
            if (self) {
              for (auto& observer : self->observers_) {
                observer.DidSuppressNavigation(url, disposition,
                                               from_user_gesture);

                // We need to check |self| to confirm that |observer| did not
                // delete |this|. If |this| is deleted, we quit.
                if (!self) {
                  return;
                }
              }
            }
          },
          weak_factory_.GetWeakPtr(), url, disposition, from_user_gesture));
}

void AshWebViewImpl::UpdateCanGoBack() {
  const bool can_go_back = web_contents_->GetController().CanGoBack();
  if (can_go_back_ == can_go_back) {
    return;
  }

  can_go_back_ = can_go_back;

  UpdateMinimizeOnBackProperty();

  for (auto& observer : observers_) {
    observer.DidChangeCanGoBack(can_go_back_);
  }
}

void AshWebViewImpl::UpdateMinimizeOnBackProperty() {
  const bool minimize_on_back = params_.minimize_on_back_key && !can_go_back_;
  views::Widget* widget = GetWidget();
  if (widget) {
    widget->GetNativeWindow()->SetProperty(ash::kMinimizeOnBackKey,
                                           minimize_on_back);
  }
}

BEGIN_METADATA(AshWebViewImpl)
END_METADATA