// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/android/dialog_overlay_impl.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/browser/renderer_host/render_widget_host_view_base.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/browser/web_contents_delegate.h"
#include "content/public/common/content_client.h"
#include "gpu/ipc/common/gpu_surface_tracker.h"
#include "media/mojo/mojom/android_overlay.mojom.h"
#include "mojo/public/cpp/bindings/sync_call_restrictions.h"
#include "third_party/abseil-cpp/absl/types/variant.h"
#include "ui/android/view_android_observer.h"
#include "ui/android/window_android.h"
// Must come after all headers that specialize FromJniType() / ToJniType().
#include "content/public/android/content_jni_headers/DialogOverlayImpl_jni.h"
using base::android::AttachCurrentThread;
using base::android::JavaParamRef;
using base::android::ScopedJavaLocalRef;
namespace content {
static jlong JNI_DialogOverlayImpl_Init(JNIEnv* env,
const JavaParamRef<jobject>& obj,
jlong high,
jlong low,
jboolean power_efficient) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
std::optional<base::UnguessableToken> token =
base::UnguessableToken::Deserialize(high, low);
if (!token.has_value()) {
return 0;
}
RenderFrameHostImpl* rfhi =
content::RenderFrameHostImpl::FromOverlayRoutingToken(token.value());
if (!rfhi)
return 0;
// If the RenderFrameHost does not have a live RenderFrame, immediately bail
// out: not only is there nothing to do, the `RenderFrameDeleted()`
// notification to clean up the overlay would never be called.
if (!rfhi->IsRenderFrameLive())
return 0;
WebContentsImpl* web_contents_impl = static_cast<WebContentsImpl*>(
content::WebContents::FromRenderFrameHost(rfhi));
// If the overlay would not be immediately used, fail the request.
if (!rfhi->IsActive() || !web_contents_impl || web_contents_impl->IsHidden())
return 0;
// Dialog-based overlays are not supported for persistent video.
if (web_contents_impl->has_persistent_video())
return 0;
// If we require a power-efficient overlay, then approximate that with "is
// fullscreen". The reason is that we want to be somewhat sure that we don't
// have more layers than HWC can support, else SurfaceFlinger will fall back
// to GLES composition. In fullscreen mode, the android status bar is hidden,
// as is the nav bar (if present). The chrome activity surface also gets
// hidden when possible.
if (power_efficient && !web_contents_impl->IsFullscreen())
return 0;
bool observe_container_view =
GetContentClient()
->browser()
->ShouldObserveContainerViewLocationForDialogOverlays();
return reinterpret_cast<jlong>(new DialogOverlayImpl(
obj, rfhi, web_contents_impl, power_efficient, observe_container_view));
}
DialogOverlayImpl::DialogOverlayImpl(const JavaParamRef<jobject>& obj,
RenderFrameHostImpl* rfhi,
WebContents* web_contents,
bool power_efficient,
bool observe_container_view)
: WebContentsObserver(web_contents),
rfhi_(rfhi),
power_efficient_(power_efficient),
observed_window_android_(false),
observe_container_view_(observe_container_view) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
DCHECK(rfhi_);
JNIEnv* env = AttachCurrentThread();
obj_ = JavaObjectWeakGlobalRef(env, obj);
// Make sure RenderFrameDeleted will be called on RFH and thus we will clean
// up.
CHECK(rfhi_->IsRenderFrameLive());
web_contents->GetNativeView()->AddObserver(this);
// Note that we're not allowed to call back into |obj| before it calls
// CompleteInit. However, the observer won't actually call us back until the
// token changes. As long as the java side calls us from the ui thread before
// returning, we won't send a callback before then.
}
void DialogOverlayImpl::CompleteInit(JNIEnv* env,
const JavaParamRef<jobject>& obj) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
WebContentsDelegate* delegate = web_contents()->GetDelegate();
if (!delegate) {
Stop();
return;
}
// Note: It's ok to call SetOverlayMode() directly here, because there can be
// at most one overlay alive at the time. This logic needs to be updated if
// ever AndroidOverlayProviderImpl.MAX_OVERLAYS > 1.
delegate->SetOverlayMode(true);
Java_DialogOverlayImpl_onWebContents(env, obj,
web_contents()->GetJavaWebContents());
// Send the initial token, if there is one. The observer will notify us about
// changes only.
if (auto* window = web_contents()->GetNativeView()->GetWindowAndroid()) {
RegisterWindowObserverIfNeeded(window);
Java_DialogOverlayImpl_onWindowAndroid(env, obj, window->GetJavaObject());
}
// Pass up a reference to the container view so we can observe its location.
// The observer will notify us if there is none yet.
StartObservingContainerView();
}
DialogOverlayImpl::~DialogOverlayImpl() {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
}
void DialogOverlayImpl::Stop() {
UnregisterCallbacksIfNeeded();
JNIEnv* env = AttachCurrentThread();
ScopedJavaLocalRef<jobject> obj = obj_.get(env);
if (!obj.is_null())
Java_DialogOverlayImpl_onDismissed(env, obj);
obj_.reset();
}
void DialogOverlayImpl::Destroy(JNIEnv* env, const JavaParamRef<jobject>& obj) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
UnregisterCallbacksIfNeeded();
// We delete soon since this might be part of an onDismissed callback.
GetUIThreadTaskRunner({})->DeleteSoon(FROM_HERE, this);
}
void DialogOverlayImpl::GetCompositorOffset(
JNIEnv* env,
const base::android::JavaParamRef<jobject>& obj,
const base::android::JavaParamRef<jobject>& rect) {
gfx::Point point =
web_contents()->GetNativeView()->GetLocationOfContainerViewInWindow();
Java_DialogOverlayImpl_receiveCompositorOffset(env, rect, point.x(),
point.y());
}
void DialogOverlayImpl::UnregisterCallbacksIfNeeded() {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
if (!rfhi_)
return;
// No need to track the container view location anymore.
StopObservingContainerView();
// We clear overlay mode here rather than in Destroy(), because we may have
// been called via a WebContentsDestroyed() event, and this might be the last
// opportunity we have to access web_contents().
WebContentsDelegate* delegate = web_contents()->GetDelegate();
if (delegate)
delegate->SetOverlayMode(false);
if (observed_window_android_) {
auto* window_android = web_contents()->GetNativeView()->GetWindowAndroid();
if (window_android)
window_android->RemoveObserver(this);
observed_window_android_ = false;
}
web_contents()->GetNativeView()->RemoveObserver(this);
rfhi_ = nullptr;
}
void DialogOverlayImpl::RenderFrameDeleted(RenderFrameHost* render_frame_host) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
if (render_frame_host == rfhi_)
Stop();
}
void DialogOverlayImpl::RenderFrameHostChanged(RenderFrameHost* old_host,
RenderFrameHost* new_host) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
if (old_host == rfhi_)
Stop();
}
void DialogOverlayImpl::OnVisibilityChanged(content::Visibility visibility) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
if (visibility == content::Visibility::HIDDEN)
Stop();
}
void DialogOverlayImpl::OnRootWindowVisibilityChanged(bool visible) {
if (!visible)
Stop();
}
void DialogOverlayImpl::WebContentsDestroyed() {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
Stop();
}
void DialogOverlayImpl::DidToggleFullscreenModeForTab(bool entered_fullscreen,
bool will_cause_resize) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
// If the caller doesn't care about power-efficient overlays, then don't send
// any callbacks about state change.
if (!power_efficient_)
return;
JNIEnv* env = AttachCurrentThread();
ScopedJavaLocalRef<jobject> obj = obj_.get(env);
if (!obj.is_null())
Java_DialogOverlayImpl_onPowerEfficientState(env, obj, entered_fullscreen);
}
void DialogOverlayImpl::OnAttachedToWindow() {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
JNIEnv* env = AttachCurrentThread();
auto* window = web_contents()->GetNativeView()->GetWindowAndroid();
if (window)
RegisterWindowObserverIfNeeded(window);
ScopedJavaLocalRef<jobject> obj = obj_.get(env);
if (!obj.is_null())
Java_DialogOverlayImpl_onWindowAndroid(env, obj, window->GetJavaObject());
StartObservingContainerView();
}
void DialogOverlayImpl::OnDetachedFromWindow() {
JNIEnv* env = AttachCurrentThread();
ScopedJavaLocalRef<jobject> obj = obj_.get(env);
if (!obj.is_null())
Java_DialogOverlayImpl_onWindowAndroid(env, obj, nullptr);
Stop();
}
void DialogOverlayImpl::RegisterWindowObserverIfNeeded(
ui::WindowAndroid* window) {
if (!observed_window_android_) {
observed_window_android_ = true;
window->AddObserver(this);
}
}
void DialogOverlayImpl::StartObservingContainerView() {
ObserveContainerViewIfNeeded(
web_contents()->GetNativeView()->GetContainerView());
}
void DialogOverlayImpl::StopObservingContainerView() {
ObserveContainerViewIfNeeded(/*container_view=*/nullptr);
}
void DialogOverlayImpl::ObserveContainerViewIfNeeded(
const ScopedJavaLocalRef<jobject>& container_view) {
if (!observe_container_view_)
return;
JNIEnv* env = AttachCurrentThread();
ScopedJavaLocalRef<jobject> obj = obj_.get(env);
if (!obj.is_null())
Java_DialogOverlayImpl_observeContainerView(env, obj, container_view);
}
// Helper class that has permission to talk to SyncCallRestrictions. Rather
// than friend the function directly, which has an odd signature, friend a class
// that knows how to do the work.
class AndroidOverlaySyncHelper {
public:
static void MakeSyncCall(media::mojom::AndroidOverlayClient* remote) {
mojo::SyncCallRestrictions::ScopedAllowSyncCall scoped_allow;
remote->OnSynchronouslyDestroyed();
}
};
static void JNI_DialogOverlayImpl_NotifyDestroyedSynchronously(
JNIEnv* env,
jlong message_pipe_handle) {
mojo::MessagePipeHandle handle(message_pipe_handle);
mojo::ScopedMessagePipeHandle scoped_handle(handle);
mojo::Remote<media::mojom::AndroidOverlayClient> remote(
mojo::PendingRemote<media::mojom::AndroidOverlayClient>(
std::move(scoped_handle),
media::mojom::AndroidOverlayClient::Version_));
// This prevents crashes, though it's unclear how we'd have a null remote.
// https://crbug.com/1155313 .
if (!remote.is_bound())
return;
AndroidOverlaySyncHelper::MakeSyncCall(remote.get());
// Note that we don't take back the mojo message pipe. We let it close when
// `remote` goes out of scope.
}
static jint JNI_DialogOverlayImpl_RegisterSurface(
JNIEnv* env,
const JavaParamRef<jobject>& surface) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
return gpu::GpuSurfaceTracker::Get()->AddSurfaceForNativeWidget(
gpu::GpuSurfaceTracker::SurfaceRecord(
gl::ScopedJavaSurface(surface, /*auto_release=*/false),
/*can_be_used_with_surface_control=*/false));
}
static void JNI_DialogOverlayImpl_UnregisterSurface(
JNIEnv* env,
jint surface_id) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
gpu::GpuSurfaceTracker::Get()->RemoveSurface(surface_id);
}
static ScopedJavaLocalRef<jobject>
JNI_DialogOverlayImpl_LookupSurfaceForTesting(
JNIEnv* env,
jint surfaceId) {
bool can_be_used_with_surface_control = false;
auto surface_variant = gpu::GpuSurfaceTracker::Get()->AcquireJavaSurface(
surfaceId, &can_be_used_with_surface_control);
if (!absl::holds_alternative<gl::ScopedJavaSurface>(surface_variant)) {
return nullptr;
}
return ScopedJavaLocalRef<jobject>(
absl::get<gl::ScopedJavaSurface>(surface_variant).j_surface());
}
} // namespace content