1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403
|
// 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.
#ifndef CHROME_BROWSER_PICTURE_IN_PICTURE_PICTURE_IN_PICTURE_WINDOW_MANAGER_H_
#define CHROME_BROWSER_PICTURE_IN_PICTURE_PICTURE_IN_PICTURE_WINDOW_MANAGER_H_
#include <functional>
#include <optional>
#include "base/memory/raw_ptr.h"
#include "base/memory/singleton.h"
#include "base/observer_list.h"
#include "base/observer_list_types.h"
#include "build/build_config.h"
#include "third_party/blink/public/mojom/picture_in_picture_window_options/picture_in_picture_window_options.mojom.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/views/bubble/bubble_border.h"
#include "url/gurl.h"
#if !BUILDFLAG(IS_ANDROID)
#include "base/types/pass_key.h"
#include "chrome/browser/picture_in_picture/auto_pip_setting_overlay_view.h"
#include "chrome/browser/picture_in_picture/picture_in_picture_window_manager_uma_helper.h"
#endif // !BUILDFLAG(IS_ANDROID)
namespace content {
enum class PictureInPictureResult;
class PictureInPictureWindowController;
class WebContents;
} // namespace content
namespace display {
class Display;
} // namespace display
#if !BUILDFLAG(IS_ANDROID)
class PictureInPictureOcclusionTracker;
class PictureInPictureWindow;
class ScopedDisallowPictureInPicture;
class ScopedTuckPictureInPicture;
namespace views {
class View;
} // namespace views
#endif
struct NavigateParams;
// PictureInPictureWindowManager is a singleton that handles the lifetime of the
// current Picture-in-Picture window and its PictureInPictureWindowController.
// The class also guarantees that only one window will be present per Chrome
// instances regardless of the number of windows, tabs, profiles, etc.
class PictureInPictureWindowManager {
public:
// Observer for PictureInPictureWindowManager events.
class Observer : public base::CheckedObserver {
public:
virtual void OnEnterPictureInPicture() {}
};
// Returns the singleton instance.
static PictureInPictureWindowManager* GetInstance();
PictureInPictureWindowManager(const PictureInPictureWindowManager&) = delete;
PictureInPictureWindowManager& operator=(
const PictureInPictureWindowManager&) = delete;
// Shows a PIP window using the window controller for a video element.
//
// This mode is triggered through WebContentsDelegate::EnterPictureInPicture,
// and the default implementation of that fails with a kNotSupported
// result. For compatibility, this method must also return a
// content::PictureInPictureResult even though it doesn't fail.
content::PictureInPictureResult EnterVideoPictureInPicture(
content::WebContents*);
#if !BUILDFLAG(IS_ANDROID)
// Shows a PIP window using the window controller for document picture in
// picture.
//
// Document picture-in-picture mode is triggered from the Renderer via
// WindowOpenDisposition::NEW_PICTURE_IN_PICTURE, and the browser
// (i.e. Chrome's BrowserNavigator) then calls this method to create the
// window. There's no corresponding path through the WebContentsDelegate, so
// it doesn't have a failure state.
void EnterDocumentPictureInPicture(content::WebContents* parent_web_contents,
content::WebContents* child_web_contents);
#endif // !BUILDFLAG(IS_ANDROID)
// Shows a PIP window with an explicitly provided window controller. This is
// used by ChromeOS ARC windows which do not have a WebContents as the source.
void EnterPictureInPictureWithController(
content::PictureInPictureWindowController* pip_window_controller);
// Expected behavior of the window UI-initiated close.
enum class UiBehavior {
// Close the window, but don't try to pause the video. This is also the
// behavior of `ExitPictureInPicture()`.
kCloseWindowOnly,
// Close the window, and also pause the video.
kCloseWindowAndPauseVideo,
// Act like the back-to-tab button: focus the opener window, and don't pause
// the video.
kCloseWindowAndFocusOpener,
};
// The user has requested to close the pip window. This is similar to
// `ExitPictureInPicture()`, except that it's strictly user-initiated via the
// window UI.
bool ExitPictureInPictureViaWindowUi(UiBehavior behavior);
// Closes any existing picture-in-picture windows (video or document pip).
// Returns true if a picture-in-picture window was closed, and false if there
// were no picture-in-picture windows to close.
bool ExitPictureInPicture();
// Called to notify that the initiator web contents should be focused.
void FocusInitiator();
// Gets the web contents in the opener browser window.
content::WebContents* GetWebContents() const;
// Gets the web contents in the PiP window. This only applies to document PiP
// and will be null for video PiP.
content::WebContents* GetChildWebContents() const;
// Helper method that will check if the given WebContents is hosted in a
// document PiP window.
static bool IsChildWebContents(content::WebContents*);
// When a website requests size `requested_size` for a document
// picture-in-picture window (either when creating the window or when resizing
// the window via resizeTo()/resizeBy() APIs), we restrict the maximum size
// we'll make the pip window (this is a smaller maximum than the maximum size
// the user can manually resize to). If the given `requested_size` is small
// enough, this just returns the requested size. Otherwise, this shrinks the
// requested size to fit within the constraints, and attempts to keep the
// aspect ratio the same.
static gfx::Size AdjustRequestedSizeIfNecessary(
const gfx::Size& requested_size,
const display::Display& display);
// Returns the window bounds of the video picture-in-picture or the document
// picture-in-picture if either of them is present.
std::optional<gfx::Rect> GetPictureInPictureWindowBounds() const;
// Used for Document picture-in-picture windows only. The returned dimensions
// represent the outer window bounds.
// This method is called from the |BrowserNavigator|. Note that the window
// bounds may be later re-adjusted by the |PictureInPictureBrowserFrameView|
// to accommodate non-client view elements, while respecting the minimum inner
// window size.
gfx::Rect CalculateInitialPictureInPictureWindowBounds(
const blink::mojom::PictureInPictureWindowOptions& pip_options,
const display::Display& display);
// Used for Document picture-in-picture windows only. The returned dimensions
// represent the outer window bounds.
// This method is called from |PictureInPictureBrowserFrameView|. Picture in
// picture window bounds are only adjusted when, the requested window size
// would cause the minimum inner window size to be smaller than the allowed
// minimum (|GetMinimumInnerWindowSize|).
gfx::Rect CalculateOuterWindowBounds(
const blink::mojom::PictureInPictureWindowOptions& pip_options,
const display::Display& display,
const gfx::Size& minimum_window_size,
const gfx::Size& excluded_margin);
// Update the most recent window bounds for the pip window in the cache. Call
// this when the pip window moves or resizes, though it's okay if not every
// update makes it here.
void UpdateCachedBounds(const gfx::Rect& most_recent_bounds);
// Clears the picture-in-picture window cached bounds.
void ClearCachedBounds();
// Used for Document picture-in-picture windows only.
// Note that this is meant to represent the inner window bounds. When the pip
// window is drawn, outer bounds may be greater than kMinWindowSize to
// accommodate window decorations and ensure the inner bound minimum size
// respects kMinWindowSize.
static gfx::Size GetMinimumInnerWindowSize();
// Used for Document picture-in-picture windows only.
static gfx::Size GetMaximumWindowSize(const display::Display& display);
// Properly sets the `window_action` on `params`.
static void SetWindowParams(NavigateParams& params);
void AddObserver(Observer* observer) { observers_.AddObserver(observer); }
void RemoveObserver(Observer* observer) {
observers_.RemoveObserver(observer);
}
// Notify observers that picture-in-picture window is created.
void NotifyObserversOnEnterPictureInPicture();
#if !BUILDFLAG(IS_ANDROID)
std::unique_ptr<AutoPipSettingOverlayView> GetOverlayView(
views::View* anchor_view,
views::BubbleBorder::Arrow arrow);
// Returns the PictureInPictureOcclusionTracker, which can inform observers
// when a widget has been occluded by a video or document picture-in-picture
// window.
PictureInPictureOcclusionTracker* GetOcclusionTracker();
// Returns true if a file dialog opened by `owner_web_contents` should create
// a `ScopedDisallowPictureInPicture` to block picture-in-picture.
bool ShouldFileDialogBlockPictureInPicture(
content::WebContents* owner_web_contents);
// Called by `ScopedDisallowPictureInPicture` to block picture-in-picture
// windows from opening, as well as close any existing picture-in-picture
// windows.
void OnScopedDisallowPictureInPictureCreated(
base::PassKey<ScopedDisallowPictureInPicture>);
void OnScopedDisallowPictureInPictureDestroyed(
base::PassKey<ScopedDisallowPictureInPicture>);
// Called by a picture-in-picture window (either video picture-in-picture or
// document picture-in-picture) when it is opened. This allows us to
// communicate directly with the window for things that can't be handled
// through the PictureInPictureWindowController.
void OnPictureInPictureWindowShown(PictureInPictureWindow* window);
// Called by a picture-in-picture window when it closes or hides to end the
// connection opened by a previous call to `OnPictureInPictureWindowShown`.
void OnPictureInPictureWindowHidden(PictureInPictureWindow* window);
// Returns true if a file dialog opened by `owner_web_contents` should create
// a `ScopedTuckPictureInPicture` to tuck picture-in-picture.
bool ShouldFileDialogTuckPictureInPicture(
content::WebContents* owner_web_contents);
// Called by `ScopedTuckPictureInPicture` to force-tuck any existing or future
// picture-in-picture windows until it's destroyed.
void OnScopedTuckPictureInPictureCreated(
base::PassKey<ScopedTuckPictureInPicture>);
void OnScopedTuckPictureInPictureDestroyed(
base::PassKey<ScopedTuckPictureInPicture>);
// Returns true if picture-in-picture windows are currently force-tucked (e.g.
// due to a ScopedTuckPictureInPicture object existing).
bool IsPictureInPictureForceTucked() const;
#endif
// Returns true if picture-in-picture is currently disabled (e.g. due to a
// ScopedDisallowPictureInPicture object existing).
bool IsPictureInPictureDisabled() const;
void set_window_controller_for_testing(
content::PictureInPictureWindowController* controller) {
pip_window_controller_ = controller;
}
// Return true if and only if the URL is can be used as an opener. This check
// allows us to explicitly opt-in opener URL types, to ensure that the pip
// window's title bar is formatted properly. Allowing any secure context, for
// example, might result in a misleading window title.
static bool IsSupportedForDocumentPictureInPicture(const GURL& url);
#if !BUILDFLAG(IS_ANDROID)
void set_uma_helper_for_testing(
std::unique_ptr<PictureInPictureWindowManagerUmaHelper> uma_helper) {
uma_helper_ = std::move(uma_helper);
}
#endif // !BUILDFLAG(IS_ANDROID)
private:
friend struct base::DefaultSingletonTraits<PictureInPictureWindowManager>;
class VideoWebContentsObserver;
#if !BUILDFLAG(IS_ANDROID)
class DocumentWebContentsObserver;
// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused.
//
// LINT.IfChange(PictureInPictureDisallowedType)
enum class PictureInPictureDisallowedType {
// An existing picture-in-picture window was closed because we started
// disallowing picture-in-picture windows.
kExistingWindowClosed = 0,
// A new picture-in-picture window was closed because it was created while
// we were already disallowing picture-in-picture windows.
kNewWindowClosed = 1,
kMaxValue = kNewWindowClosed,
};
// LINT.ThenChange(//tools/metrics/histograms/metadata/media/enums.xml:PictureInPictureDisallowedTypeEnum)
// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused.
//
// LINT.IfChange(PictureInPictureTuckedType)
enum class PictureInPictureTuckedType {
// An existing picture-in-picture window was tucked because we started
// force-tucking picture-in-picture windows.
kExistingWindowTucked = 0,
// A new picture-in-picture window was tucked because it was created while
// we were already force-tucking picture-in-picture windows.
kNewWindowTucked = 1,
kMaxValue = kNewWindowTucked,
};
// LINT.ThenChange(//tools/metrics/histograms/metadata/media/enums.xml:PictureInPictureTuckedTypeEnum)
#endif // !BUILDFLAG(IS_ANDROID)
// Create a Picture-in-Picture window and register it in order to be closed
// when needed.
// This is suffixed with "Internal" because `CreateWindow` is part of the
// Windows API.
void CreateWindowInternal(content::WebContents*);
// Closes the active Picture-in-Picture window.
// There MUST be a window open.
// This is suffixed with "Internal" to keep consistency with the method above.
void CloseWindowInternal();
#if !BUILDFLAG(IS_ANDROID)
// Called when the document PiP parent web contents is being destroyed.
void DocumentWebContentsDestroyed();
#endif // !BUILDFLAG(IS_ANDROID)
// Exits picture in picture soon, but not before this call returns. If
// picture in picture closes between now and then, that's okay. Intended as a
// helper class for callbacks, to avoid re-entrant calls during pip set-up.
static void ExitPictureInPictureSoon();
#if !BUILDFLAG(IS_ANDROID)
// Creates the `occlusion_tracker_` if it does not already exist and should
// exist.
void CreateOcclusionTrackerIfNecessary();
// Records metrics about the requested size of a document picture-in-picture
// window.
void RecordDocumentPictureInPictureRequestedSizeMetrics(
const blink::mojom::PictureInPictureWindowOptions& pip_options,
const display::Display& display);
// Records whether a new or existing picture-in-picture window was closed due
// to an existing ScopedDisallowPictureInPicture.
void RecordPictureInPictureDisallowed(PictureInPictureDisallowedType type);
// Records whether a new or existing picture-in-picture window was tucked due
// to an existing ScopedTuckPictureInPicture.
void RecordPictureInPictureTucked(PictureInPictureTuckedType type);
#endif // !BUILDFLAG(IS_ANDROID)
#if !BUILDFLAG(IS_ANDROID)
// Records the total time spent on a picture in picture window, regardless of
// the Picture-in-Picture window type (document vs video) and the reason for
// closing the window (UI interaction, returning back to opener tab, etc.).
//
// The metric is recorded using the `PictureInPictureWindowManagerUmaHelper`,
// which this method will create if one does not already exist.
void MaybeRecordPictureInPictureChanged(bool is_picture_in_picture);
#endif // !BUILDFLAG(IS_ANDROID)
PictureInPictureWindowManager();
~PictureInPictureWindowManager();
// Observers that listen to updates of this instance.
base::ObserverList<Observer> observers_;
std::unique_ptr<VideoWebContentsObserver> video_web_contents_observer_;
#if !BUILDFLAG(IS_ANDROID)
std::unique_ptr<DocumentWebContentsObserver> document_web_contents_observer_;
std::unique_ptr<PictureInPictureOcclusionTracker> occlusion_tracker_;
// The number of `ScopedDisallowPictureInPicture` objects currently in
// existence. If at least one exists, then picture-in-picture windows will be
// blocked.
uint32_t number_of_existing_scoped_disallow_picture_in_pictures_ = 0;
// True if we're currently calculating document pip's initial size. Used to
// determine whether we should record the
// `Media.DocumentPictureInPicture.RequestedLargeInitialSize` metric and
// should be removed when that metric is removed.
bool is_calculating_initial_document_pip_size_ = false;
// The number of `ScopedTuckPictureInPicture` objects currently in
// existence. If at least one exists, then picture-in-picture windows will be
// tucked.
uint32_t number_of_existing_scoped_tuck_picture_in_pictures_ = 0;
// Pointer to the currently shown picture-in-picture window, if any.
raw_ptr<PictureInPictureWindow> picture_in_picture_window_ = nullptr;
std::unique_ptr<PictureInPictureWindowManagerUmaHelper> uma_helper_;
#endif //! BUILDFLAG(IS_ANDROID)
raw_ptr<content::PictureInPictureWindowController, DanglingUntriaged>
pip_window_controller_ = nullptr;
};
#endif // CHROME_BROWSER_PICTURE_IN_PICTURE_PICTURE_IN_PICTURE_WINDOW_MANAGER_H_
|