| // Copyright 2018 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/android/overlay/overlay_window_android.h" |
| |
| #include "base/android/jni_android.h" |
| #include "base/android/jni_array.h" |
| #include "base/android/unguessable_token_android.h" |
| #include "base/memory/ptr_util.h" |
| #include "base/no_destructor.h" |
| #include "cc/slim/surface_layer.h" |
| #include "chrome/android/chrome_jni_headers/PictureInPictureActivity_jni.h" |
| #include "chrome/browser/android/tab_android.h" |
| #include "components/thin_webview/compositor_view.h" |
| #include "content/public/browser/overlay_window.h" |
| #include "content/public/browser/video_picture_in_picture_window_controller.h" |
| #include "content/public/browser/web_contents.h" |
| #include "media/base/media_switches.h" |
| #include "ui/android/window_android_compositor.h" |
| |
| using WindowMap = base::flat_map<base::UnguessableToken, OverlayWindowAndroid*>; |
| |
| namespace { |
| WindowMap& GetWindowMap() { |
| static base::NoDestructor<WindowMap> instance; |
| return *instance; |
| } |
| |
| } // namespace |
| |
| // static |
| std::unique_ptr<content::VideoOverlayWindow> |
| content::VideoOverlayWindow::Create( |
| VideoPictureInPictureWindowController* controller) { |
| return std::make_unique<OverlayWindowAndroid>(controller); |
| } |
| |
| OverlayWindowAndroid::OverlayWindowAndroid( |
| content::VideoPictureInPictureWindowController* controller) |
| : window_android_(nullptr), |
| compositor_view_(nullptr), |
| surface_layer_(cc::slim::SurfaceLayer::Create()), |
| bounds_(gfx::Rect(0, 0)), |
| update_action_timer_(std::make_unique<base::OneShotTimer>()), |
| controller_(controller) { |
| GetWindowMap().emplace(token_, this); |
| surface_layer_->SetIsDrawable(true); |
| surface_layer_->SetStretchContentToFillBounds(true); |
| surface_layer_->SetBackgroundColor(SkColors::kBlack); |
| |
| auto* web_contents = controller_->GetWebContents(); |
| |
| // Compute the screen position of the video, and see if it fits inside the |
| // WebContents or if it's clipped / off-screen. If it's onscreen, then T and |
| // later, Android can do a nicer animated transition to PiP with a screen |
| // capture of the video. However, if the video is clipped / offscreen, then |
| // it'll look nicer to use the default light grey transition. |
| |
| // We provide a small buffer for what "clipped" means, rather than enforcing |
| // it strictly. It'll still look fine while allowing small positioning errors |
| // that sites sometimes make. See https://crbug.com/1411517 for an example. |
| |
| // The java side will ignore any source bounds that are not on the screen for |
| // the source rect hint. It will use the aspect ratio only in that case. We |
| // set the x position to be <0 to ensure this, to skip the transition. |
| |
| // Get the size of the video, and inset it to provide some slack. |
| gfx::Rect source_bounds = controller_->GetSourceBounds(); |
| gfx::Rect smaller_source_bounds = source_bounds; |
| constexpr int inset_size = 4; // pixels on each side |
| smaller_source_bounds.Inset(inset_size); |
| |
| // Get the size of the WebContents, and convert to pixels. |
| gfx::Rect unscaled_content_bounds = web_contents->GetContainerBounds(); |
| auto* native_view = web_contents->GetNativeView(); |
| const float dip_scale = native_view->GetDipScale(); |
| gfx::Rect content_bounds(unscaled_content_bounds.x() * dip_scale, |
| unscaled_content_bounds.y() * dip_scale, |
| unscaled_content_bounds.width() * dip_scale, |
| unscaled_content_bounds.height() * dip_scale); |
| const bool out_of_bounds = !content_bounds.Contains(smaller_source_bounds); |
| // Use the new source location based transition when the source is not out of |
| // bound and the AllowEnhancedPipTransition feature is enabled. |
| // TODO(crbug.com/440384447): remove AllowEnhancedPipTransition check once the |
| // new transition works properly on desktop Android. |
| const bool use_source_hint_transition = |
| !out_of_bounds && |
| base::FeatureList::IsEnabled(media::kAllowEnhancedPipTransition); |
| |
| if (use_source_hint_transition) { |
| // Use the newer transition, if available. |
| // Convert to screen space. Since the comparison was with the inset source |
| // bounds, clamp the real source bounds to the container. |
| source_bounds.Intersect(content_bounds); |
| gfx::PointF offset = native_view->GetLocationOnScreen(0, 0); |
| source_bounds.Offset( |
| static_cast<int>(offset.x()), |
| static_cast<int>(offset.y()) + |
| native_view->content_offset() * native_view->GetDipScale()); |
| } else { |
| // Use the old transition. |
| // Slide this offscreen, while keeping the aspect ratio the same. |
| source_bounds.set_x(-1); |
| } |
| |
| JNIEnv* env = base::android::AttachCurrentThread(); |
| auto j_token = base::android::UnguessableTokenAndroid::Create(env, token_); |
| Java_PictureInPictureActivity_createActivity( |
| env, j_token, TabAndroid::FromWebContents(web_contents)->GetJavaObject(), |
| source_bounds.x(), source_bounds.y(), source_bounds.width(), |
| source_bounds.height()); |
| } |
| |
| OverlayWindowAndroid::~OverlayWindowAndroid() { |
| // Any future use of our token will fail. |
| GetWindowMap().erase(token_); |
| if (java_ref_.is_uninitialized()) { |
| return; |
| } |
| // Notify the java side that the native side is gone. |
| JNIEnv* env = base::android::AttachCurrentThread(); |
| Java_PictureInPictureActivity_onWindowDestroyed(env, java_ref_.get(env)); |
| } |
| |
| static jlong JNI_PictureInPictureActivity_OnActivityStart( |
| JNIEnv* env, |
| const jni_zero::JavaParamRef<jobject>& j_token, |
| const jni_zero::JavaParamRef<jobject>& self, |
| const jni_zero::JavaParamRef<jobject>& window) { |
| auto token = base::android::UnguessableTokenAndroid::FromJavaUnguessableToken( |
| env, j_token); |
| auto iter = GetWindowMap().find(token); |
| if (iter == GetWindowMap().end()) { |
| return 0; |
| } |
| OverlayWindowAndroid* thiz = iter->second; |
| thiz->Initialize(env, self, window); |
| |
| return reinterpret_cast<jlong>(thiz); |
| } |
| |
| void OverlayWindowAndroid::Initialize( |
| JNIEnv* env, |
| const base::android::JavaParamRef<jobject>& self, |
| const base::android::JavaParamRef<jobject>& jwindow_android) { |
| java_ref_ = JavaObjectWeakGlobalRef(env, self); |
| window_android_ = ui::WindowAndroid::FromJavaWindowAndroid(jwindow_android); |
| window_android_->AddObserver(this); |
| |
| Java_PictureInPictureActivity_setPlaybackState(env, java_ref_.get(env), |
| playback_state_); |
| Java_PictureInPictureActivity_setMicrophoneMuted(env, java_ref_.get(env), |
| microphone_muted_); |
| Java_PictureInPictureActivity_setCameraState(env, java_ref_.get(env), |
| camera_on_); |
| |
| if (!update_action_timer_->IsRunning()) { |
| MaybeNotifyVisibleActionsChanged(); |
| } |
| |
| if (video_size_.IsEmpty()) { |
| return; |
| } |
| |
| Java_PictureInPictureActivity_updateVideoSize( |
| env, java_ref_.get(env), video_size_.width(), video_size_.height()); |
| } |
| |
| void OverlayWindowAndroid::OnAttachCompositor() { |
| window_android_->GetCompositor()->AddChildFrameSink( |
| surface_layer_->surface_id().frame_sink_id()); |
| } |
| |
| void OverlayWindowAndroid::OnDetachCompositor() { |
| window_android_->GetCompositor()->RemoveChildFrameSink( |
| surface_layer_->surface_id().frame_sink_id()); |
| } |
| |
| void OverlayWindowAndroid::OnActivityStopped() { |
| if (java_ref_.is_uninitialized()) { |
| return; |
| } |
| |
| // If the activity stops, pretend that somebody pressed the close button. |
| // This will notify the java side to forget about us, and clean up. |
| Close(); |
| // `this` may be destroyed. |
| } |
| |
| void OverlayWindowAndroid::DestroyStartedByJava(JNIEnv* env) { |
| // Note that the java side also clears its native ptr when calling us, so it's |
| // okay that we don't notify it in the dtor. |
| // ** IMPORTANT ** Do not add calls here unless the above statement continues |
| // to be true. It's unlikely that anything on the native side should call |
| // this method directly. |
| java_ref_.reset(); |
| |
| // Stop the timer for completeness, though resetting `java_ref_` will make it |
| // a no-op. |
| update_action_timer_->Stop(); |
| |
| if (window_android_) { |
| window_android_->RemoveObserver(this); |
| window_android_ = nullptr; |
| } |
| |
| // Only pause the video when play/pause button is visible. |
| controller_->OnWindowDestroyed( |
| /*should_pause_video=*/visible_actions_.find( |
| static_cast<int>(media_session::mojom::MediaSessionAction::kPlay)) != |
| visible_actions_.end()); |
| // `this` may be destroyed. |
| } |
| |
| void OverlayWindowAndroid::TogglePlayPause(JNIEnv* env, bool toggleOn) { |
| DCHECK(!controller_->IsPlayerActive()); |
| if (toggleOn == (playback_state_ == PlaybackState::kPaused)) { |
| controller_->TogglePlayPause(); |
| } |
| } |
| |
| void OverlayWindowAndroid::NextTrack(JNIEnv* env) { |
| controller_->NextTrack(); |
| } |
| |
| void OverlayWindowAndroid::PreviousTrack(JNIEnv* env) { |
| controller_->PreviousTrack(); |
| } |
| |
| void OverlayWindowAndroid::NextSlide(JNIEnv* env) { |
| controller_->NextSlide(); |
| } |
| |
| void OverlayWindowAndroid::PreviousSlide(JNIEnv* env) { |
| controller_->PreviousSlide(); |
| } |
| |
| void OverlayWindowAndroid::ToggleMicrophone(JNIEnv* env, bool toggleOn) { |
| if (microphone_muted_ == toggleOn) { |
| controller_->ToggleMicrophone(); |
| } |
| } |
| |
| void OverlayWindowAndroid::ToggleCamera(JNIEnv* env, bool toggleOn) { |
| if (!camera_on_ == toggleOn) { |
| controller_->ToggleCamera(); |
| } |
| } |
| |
| void OverlayWindowAndroid::HangUp(JNIEnv* env) { |
| controller_->HangUp(); |
| } |
| |
| void OverlayWindowAndroid::CompositorViewCreated( |
| JNIEnv* env, |
| const base::android::JavaParamRef<jobject>& compositor_view) { |
| compositor_view_ = |
| thin_webview::android::CompositorView::FromJavaObject(compositor_view); |
| DCHECK(compositor_view_); |
| compositor_view_->SetRootLayer(surface_layer_); |
| } |
| |
| void OverlayWindowAndroid::OnViewSizeChanged(JNIEnv* env, |
| jint width, |
| jint height) { |
| gfx::Size content_size(width, height); |
| if (bounds_.size() == content_size) { |
| return; |
| } |
| |
| bounds_.set_size(content_size); |
| surface_layer_->SetBounds(content_size); |
| controller_->UpdateLayerBounds(); |
| } |
| |
| void OverlayWindowAndroid::OnBackToTab(JNIEnv* env) { |
| controller_->FocusInitiator(); |
| Hide(); |
| } |
| |
| void OverlayWindowAndroid::Close() { |
| CloseInternal(); |
| controller_->OnWindowDestroyed(/*should_pause_video=*/true); |
| // `this` may be destroyed. |
| } |
| |
| void OverlayWindowAndroid::Hide() { |
| CloseInternal(); |
| controller_->OnWindowDestroyed(/*should_pause_video=*/false); |
| // `this` may be destroyed. |
| } |
| |
| void OverlayWindowAndroid::CloseInternal() { |
| if (java_ref_.is_uninitialized()) { |
| return; |
| } |
| |
| DCHECK(window_android_); |
| window_android_->RemoveObserver(this); |
| window_android_ = nullptr; |
| JNIEnv* env = base::android::AttachCurrentThread(); |
| Java_PictureInPictureActivity_close(env, java_ref_.get(env)); |
| // The java side forgets about us on close, so don't call back. |
| java_ref_.reset(); |
| |
| // Stop any in-flight action button updates. We won't find out if the Android |
| // window is destroyed since that comes from `WindowAndroidObserver` but we |
| // just unregistered from that. |
| update_action_timer_->Stop(); |
| } |
| |
| bool OverlayWindowAndroid::IsActive() const { |
| return true; |
| } |
| |
| bool OverlayWindowAndroid::IsVisible() const { |
| return true; |
| } |
| |
| gfx::Rect OverlayWindowAndroid::GetBounds() { |
| return bounds_; |
| } |
| |
| void OverlayWindowAndroid::UpdateNaturalSize(const gfx::Size& natural_size) { |
| if (java_ref_.is_uninitialized()) { |
| video_size_ = natural_size; |
| // This isn't guaranteed to be right, but it's better than (0,0). |
| bounds_.set_size(natural_size); |
| return; |
| } |
| |
| JNIEnv* env = base::android::AttachCurrentThread(); |
| Java_PictureInPictureActivity_updateVideoSize( |
| env, java_ref_.get(env), natural_size.width(), natural_size.height()); |
| } |
| |
| void OverlayWindowAndroid::SetPlaybackState(PlaybackState playback_state) { |
| if (playback_state_ == playback_state) { |
| return; |
| } |
| |
| playback_state_ = playback_state; |
| if (java_ref_.is_uninitialized()) { |
| return; |
| } |
| |
| JNIEnv* env = base::android::AttachCurrentThread(); |
| Java_PictureInPictureActivity_setPlaybackState(env, java_ref_.get(env), |
| playback_state); |
| } |
| |
| void OverlayWindowAndroid::SetMicrophoneMuted(bool muted) { |
| if (microphone_muted_ == muted) { |
| return; |
| } |
| |
| microphone_muted_ = muted; |
| if (java_ref_.is_uninitialized()) { |
| return; |
| } |
| |
| JNIEnv* env = base::android::AttachCurrentThread(); |
| Java_PictureInPictureActivity_setMicrophoneMuted(env, java_ref_.get(env), |
| microphone_muted_); |
| } |
| |
| void OverlayWindowAndroid::SetCameraState(bool turned_on) { |
| if (camera_on_ == turned_on) { |
| return; |
| } |
| |
| camera_on_ = turned_on; |
| if (java_ref_.is_uninitialized()) { |
| return; |
| } |
| |
| JNIEnv* env = base::android::AttachCurrentThread(); |
| Java_PictureInPictureActivity_setCameraState(env, java_ref_.get(env), |
| camera_on_); |
| } |
| |
| void OverlayWindowAndroid::SetPlayPauseButtonVisibility(bool is_visible) { |
| MaybeUpdateVisibleAction(media_session::mojom::MediaSessionAction::kPlay, |
| is_visible); |
| } |
| |
| void OverlayWindowAndroid::SetNextTrackButtonVisibility(bool is_visible) { |
| MaybeUpdateVisibleAction(media_session::mojom::MediaSessionAction::kNextTrack, |
| is_visible); |
| } |
| |
| void OverlayWindowAndroid::SetPreviousTrackButtonVisibility(bool is_visible) { |
| MaybeUpdateVisibleAction( |
| media_session::mojom::MediaSessionAction::kPreviousTrack, is_visible); |
| } |
| |
| void OverlayWindowAndroid::SetToggleMicrophoneButtonVisibility( |
| bool is_visible) { |
| MaybeUpdateVisibleAction( |
| media_session::mojom::MediaSessionAction::kToggleMicrophone, is_visible); |
| } |
| |
| void OverlayWindowAndroid::SetToggleCameraButtonVisibility(bool is_visible) { |
| MaybeUpdateVisibleAction( |
| media_session::mojom::MediaSessionAction::kToggleCamera, is_visible); |
| } |
| |
| void OverlayWindowAndroid::SetHangUpButtonVisibility(bool is_visible) { |
| MaybeUpdateVisibleAction(media_session::mojom::MediaSessionAction::kHangUp, |
| is_visible); |
| } |
| |
| void OverlayWindowAndroid::SetNextSlideButtonVisibility(bool is_visible) { |
| MaybeUpdateVisibleAction(media_session::mojom::MediaSessionAction::kNextSlide, |
| is_visible); |
| } |
| |
| void OverlayWindowAndroid::SetPreviousSlideButtonVisibility(bool is_visible) { |
| MaybeUpdateVisibleAction( |
| media_session::mojom::MediaSessionAction::kPreviousSlide, is_visible); |
| } |
| |
| void OverlayWindowAndroid::SetSurfaceId(const viz::SurfaceId& surface_id) { |
| const viz::SurfaceId& old_surface_id = surface_layer_->surface_id().is_valid() |
| ? surface_layer_->surface_id() |
| : surface_id; |
| if (window_android_ && window_android_->GetCompositor() && |
| old_surface_id.frame_sink_id() != surface_id.frame_sink_id()) { |
| // On Android, the new frame sink needs to be added before |
| // removing the previous surface sink. |
| window_android_->GetCompositor()->AddChildFrameSink( |
| surface_id.frame_sink_id()); |
| window_android_->GetCompositor()->RemoveChildFrameSink( |
| old_surface_id.frame_sink_id()); |
| } |
| // Set the surface after frame sink hierarchy update. |
| surface_layer_->SetSurfaceId(surface_id, |
| cc::DeadlinePolicy::UseDefaultDeadline()); |
| } |
| |
| void OverlayWindowAndroid::MaybeNotifyVisibleActionsChanged() { |
| if (java_ref_.is_uninitialized()) { |
| return; |
| } |
| |
| JNIEnv* env = base::android::AttachCurrentThread(); |
| Java_PictureInPictureActivity_updateVisibleActions( |
| env, java_ref_.get(env), |
| base::android::ToJavaIntArray( |
| env, |
| std::vector<int>(visible_actions_.begin(), visible_actions_.end()))); |
| } |
| |
| void OverlayWindowAndroid::MaybeUpdateVisibleAction( |
| const media_session::mojom::MediaSessionAction& action, |
| bool is_visible) { |
| int action_code = static_cast<int>(action); |
| if ((visible_actions_.find(action_code) != visible_actions_.end()) == |
| is_visible) { |
| return; |
| } |
| |
| if (is_visible) { |
| visible_actions_.insert(action_code); |
| } else { |
| visible_actions_.erase(action_code); |
| } |
| |
| if (!update_action_timer_->IsRunning()) { |
| update_action_timer_->Start( |
| FROM_HERE, base::Seconds(1), |
| base::BindOnce(&OverlayWindowAndroid::MaybeNotifyVisibleActionsChanged, |
| base::Unretained(this))); |
| } |
| } |