blob: 58d09e5f44a77cc5a0e5c735e247c452f8505b76 [file] [log] [blame]
/*
* Copyright (C) 2018 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
#import "config.h"
#if ENABLE(FULLSCREEN_API) && PLATFORM(IOS)
#import "WKFullScreenViewController.h"
#import "FullscreenTouchSecheuristic.h"
#import "PlaybackSessionManagerProxy.h"
#import "UIKitSPI.h"
#import "WKFullscreenStackView.h"
#import "WKWebViewInternal.h"
#import "WebFullScreenManagerProxy.h"
#import "WebPageProxy.h"
#import <WebCore/LocalizedStrings.h>
#import <wtf/RetainPtr.h>
using namespace WebCore;
using namespace WebKit;
static const NSTimeInterval showHideAnimationDuration = 0.1;
static const NSTimeInterval autoHideDelay = 4.0;
static const double requiredScore = 0.1;
@class WKFullscreenStackView;
class WKFullScreenViewControllerPlaybackSessionModelClient : PlaybackSessionModelClient {
public:
void setParent(WKFullScreenViewController *parent) { m_parent = parent; }
void rateChanged(bool isPlaying, float) override
{
m_parent.playing = isPlaying;
}
void pictureInPictureActiveChanged(bool active) override
{
m_parent.pictureInPictureActive = active;
}
void setInterface(PlaybackSessionInterfaceAVKit* interface)
{
if (m_interface == interface)
return;
if (m_interface && m_interface->playbackSessionModel())
interface->playbackSessionModel()->removeClient(*this);
m_interface = interface;
if (m_interface && m_interface->playbackSessionModel())
interface->playbackSessionModel()->addClient(*this);
}
private:
WKFullScreenViewController *m_parent { nullptr };
RefPtr<PlaybackSessionInterfaceAVKit> m_interface;
};
#pragma mark - _WKExtrinsicButton
@interface _WKExtrinsicButton : UIButton
@property (assign, nonatomic) CGSize extrinsicContentSize;
@end
@implementation _WKExtrinsicButton
- (void)setExtrinsicContentSize:(CGSize)size
{
_extrinsicContentSize = size;
[self invalidateIntrinsicContentSize];
}
- (CGSize)intrinsicContentSize
{
return _extrinsicContentSize;
}
@end
#pragma mark - WKFullScreenViewController
@interface WKFullScreenViewController () <UIGestureRecognizerDelegate, UIToolbarDelegate>
@property (assign, nonatomic) WKWebView *_webView; // Cannot be retained, see <rdar://problem/14884666>.
@property (readonly, nonatomic) WebFullScreenManagerProxy* _manager;
@property (readonly, nonatomic) CGFloat _effectiveFullscreenInsetTop;
@end
@implementation WKFullScreenViewController {
RetainPtr<UILongPressGestureRecognizer> _touchGestureRecognizer;
RetainPtr<WKFullscreenStackView> _stackView;
RetainPtr<_WKExtrinsicButton> _cancelButton;
RetainPtr<_WKExtrinsicButton> _pipButton;
RetainPtr<UIButton> _locationButton;
RetainPtr<UILayoutGuide> _topGuide;
RetainPtr<NSLayoutConstraint> _topConstraint;
WebKit::FullscreenTouchSecheuristic _secheuristic;
WKFullScreenViewControllerPlaybackSessionModelClient _playbackClient;
CGFloat _nonZeroStatusBarHeight;
}
#pragma mark - External Interface
- (id)initWithWebView:(WKWebView *)webView
{
self = [super init];
if (!self)
return nil;
_nonZeroStatusBarHeight = UIApplication.sharedApplication.statusBarFrame.size.height;
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_statusBarFrameDidChange:) name:UIApplicationDidChangeStatusBarFrameNotification object:nil];
_secheuristic.setRampUpSpeed(Seconds(0.25));
_secheuristic.setRampDownSpeed(Seconds(1.));
_secheuristic.setXWeight(0);
_secheuristic.setGamma(0.1);
_secheuristic.setGammaCutoff(0.08);
self._webView = webView;
_playbackClient.setParent(self);
return self;
}
- (void)dealloc
{
[NSObject cancelPreviousPerformRequestsWithTarget:self];
[[NSNotificationCenter defaultCenter] removeObserver:self];
_playbackClient.setInterface(nullptr);
[super dealloc];
}
- (void)showUI
{
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(hideUI) object:nil];
if (_playing) {
NSTimeInterval hideDelay = autoHideDelay;
[self performSelector:@selector(hideUI) withObject:nil afterDelay:hideDelay];
}
[UIView animateWithDuration:showHideAnimationDuration animations:^{
[_stackView setHidden:NO];
[_stackView setAlpha:1];
self.prefersStatusBarHidden = NO;
}];
}
- (void)hideUI
{
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(hideUI) object:nil];
[UIView animateWithDuration:showHideAnimationDuration animations:^{
[self.view removeConstraints:@[_topConstraint.get()]];
_topConstraint = [[_topGuide topAnchor] constraintEqualToAnchor:self.view.safeAreaLayoutGuide.topAnchor constant:self.view.safeAreaInsets.top];
[_topConstraint setActive:YES];
[_stackView setAlpha:0];
self.prefersStatusBarHidden = YES;
} completion:^(BOOL finished) {
if (!finished)
return;
[self.view removeConstraints:@[_topConstraint.get()]];
_topConstraint = [[_topGuide topAnchor] constraintEqualToAnchor:self.view.safeAreaLayoutGuide.topAnchor];
[_topConstraint setActive:YES];
[_stackView setHidden:YES];
}];
}
- (void)videoControlsManagerDidChange
{
WebPageProxy* page = [self._webView _page];
PlaybackSessionManagerProxy* playbackSessionManager = page ? page->playbackSessionManager() : nullptr;
PlatformPlaybackSessionInterface* playbackSessionInterface = playbackSessionManager ? playbackSessionManager->controlsManagerInterface() : nullptr;
_playbackClient.setInterface(playbackSessionInterface);
PlaybackSessionModel* playbackSessionModel = playbackSessionInterface ? playbackSessionInterface->playbackSessionModel() : nullptr;
self.playing = playbackSessionModel ? playbackSessionModel->isPlaying() : NO;
[_pipButton setHidden:!playbackSessionModel];
}
@synthesize prefersStatusBarHidden=_prefersStatusBarHidden;
- (void)setPrefersStatusBarHidden:(BOOL)value
{
_prefersStatusBarHidden = value;
[self setNeedsStatusBarAppearanceUpdate];
[self _updateWebViewFullscreenInsets];
}
- (void)setPlaying:(BOOL)isPlaying
{
if (_playing == isPlaying)
return;
_playing = isPlaying;
if (!_playing)
[self showUI];
else {
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(hideUI) object:nil];
NSTimeInterval hideDelay = autoHideDelay;
[self performSelector:@selector(hideUI) withObject:nil afterDelay:hideDelay];
}
}
- (void)setPictureInPictureActive:(BOOL)active
{
if (_pictureInPictureActive == active)
return;
_pictureInPictureActive = active;
[_pipButton setSelected:active];
}
#pragma mark - UIViewController Overrides
- (void)loadView
{
[self setView:adoptNS([[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)]).get()];
self.view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
_cancelButton = [_WKExtrinsicButton buttonWithType:UIButtonTypeSystem];
[_cancelButton setTranslatesAutoresizingMaskIntoConstraints:NO];
[_cancelButton setAdjustsImageWhenHighlighted:NO];
[_cancelButton setExtrinsicContentSize:CGSizeMake(60.0, 47.0)];
[WKFullscreenStackView applyPrimaryGlyphTintToView:_cancelButton.get()];
NSBundle *bundle = [NSBundle bundleForClass:self.class];
UIImage *doneImage = [UIImage imageNamed:@"Done" inBundle:bundle compatibleWithTraitCollection:nil];
[_cancelButton setImage:[doneImage imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate] forState:UIControlStateNormal];
[_cancelButton setTintColor:[UIColor whiteColor]];
[_cancelButton sizeToFit];
[_cancelButton addTarget:self action:@selector(_cancelAction:) forControlEvents:UIControlEventTouchUpInside];
_pipButton = [_WKExtrinsicButton buttonWithType:UIButtonTypeSystem];
[_pipButton setTranslatesAutoresizingMaskIntoConstraints:NO];
[_pipButton setAdjustsImageWhenHighlighted:NO];
[_pipButton setExtrinsicContentSize:CGSizeMake(60.0, 47.0)];
[WKFullscreenStackView applyPrimaryGlyphTintToView:_pipButton.get()];
UIImage *startPiPImage = [UIImage imageNamed:@"StartPictureInPictureButton" inBundle:bundle compatibleWithTraitCollection:nil];
UIImage *stopPiPImage = [UIImage imageNamed:@"StopPictureInPictureButton" inBundle:bundle compatibleWithTraitCollection:nil];
[_pipButton setImage:[startPiPImage imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate] forState:UIControlStateNormal];
[_pipButton setImage:[stopPiPImage imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate] forState:UIControlStateSelected];
[_pipButton setTintColor:[UIColor whiteColor]];
[_pipButton sizeToFit];
[_pipButton addTarget:self action:@selector(_togglePiPAction:) forControlEvents:UIControlEventTouchUpInside];
_stackView = adoptNS([[WKFullscreenStackView alloc] initWithArrangedSubviews:@[_cancelButton.get(), _pipButton.get()] axis:UILayoutConstraintAxisHorizontal]);
[_stackView setTranslatesAutoresizingMaskIntoConstraints:NO];
[_stackView setTargetViewForSecondaryMaterialOverlay:_cancelButton.get()];
[[self view] addSubview:_stackView.get()];
UILayoutGuide *safeArea = self.view.safeAreaLayoutGuide;
UILayoutGuide *margins = self.view.layoutMarginsGuide;
_topGuide = adoptNS([[UILayoutGuide alloc] init]);
[self.view addLayoutGuide:_topGuide.get()];
NSLayoutAnchor *topAnchor = [_topGuide topAnchor];
_topConstraint = [topAnchor constraintEqualToAnchor:safeArea.topAnchor];
[NSLayoutConstraint activateConstraints:@[
_topConstraint.get(),
[[_stackView topAnchor] constraintEqualToAnchor:topAnchor],
[[_stackView leadingAnchor] constraintEqualToAnchor:margins.leadingAnchor],
]];
[_stackView setAlpha:0];
[_stackView setHidden:YES];
[self videoControlsManagerDidChange];
_touchGestureRecognizer = adoptNS([[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(_touchDetected:)]);
[_touchGestureRecognizer setCancelsTouchesInView:NO];
[_touchGestureRecognizer setMinimumPressDuration:0];
[_touchGestureRecognizer setDelegate:self];
[self.view addGestureRecognizer:_touchGestureRecognizer.get()];
}
- (void)viewWillAppear:(BOOL)animated
{
self._webView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
self._webView.frame = self.view.bounds;
[self.view insertSubview:self._webView atIndex:0];
if (auto* manager = self._manager)
manager->setFullscreenAutoHideDelay(autoHideDelay);
}
- (void)viewDidLayoutSubviews
{
[self _updateWebViewFullscreenInsets];
_secheuristic.setSize(self.view.bounds.size);
}
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
{
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
[coordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
[self._webView _beginAnimatedResizeWithUpdates:^{
[self._webView _overrideLayoutParametersWithMinimumLayoutSize:size maximumUnobscuredSizeOverride:size];
}];
[self._webView _setInterfaceOrientationOverride:[UIApp statusBarOrientation]];
} completion:^(id <UIViewControllerTransitionCoordinatorContext>context) {
[self._webView _endAnimatedResize];
}];
}
- (UIStatusBarStyle)preferredStatusBarStyle
{
return UIStatusBarStyleLightContent;
}
- (BOOL)prefersStatusBarHidden
{
return _prefersStatusBarHidden;
}
#pragma mark - UIGestureRecognizerDelegate
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
return YES;
}
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
[self showUI];
return YES;
}
#pragma mark - Internal Interface
@dynamic _manager;
- (WebFullScreenManagerProxy*)_manager
{
if (auto* page = [self._webView _page])
return page->fullScreenManager();
return nullptr;
}
@dynamic _effectiveFullscreenInsetTop;
- (CGFloat)_effectiveFullscreenInsetTop
{
if (!self.prefersStatusBarHidden)
return 0;
CGRect cancelFrame = _cancelButton.get().frame;
CGPoint maxXY = CGPointMake(CGRectGetMaxX(cancelFrame), CGRectGetMaxY(cancelFrame));
return [_cancelButton convertPoint:maxXY toView:self.view].y;
}
- (void)_cancelAction:(id)sender
{
[[self target] performSelector:[self action]];
}
- (void)_togglePiPAction:(id)sender
{
WebPageProxy* page = [self._webView _page];
if (!page)
return;
PlaybackSessionManagerProxy* playbackSessionManager = page->playbackSessionManager();
if (!playbackSessionManager)
return;
PlatformPlaybackSessionInterface* playbackSessionInterface = playbackSessionManager->controlsManagerInterface();
if (!playbackSessionInterface)
return;
PlaybackSessionModel* playbackSessionModel = playbackSessionInterface->playbackSessionModel();
if (!playbackSessionModel)
return;
playbackSessionModel->togglePictureInPicture();
}
- (void)_touchDetected:(id)sender
{
if ([_touchGestureRecognizer state] != UIGestureRecognizerStateBegan || [_touchGestureRecognizer state] == UIGestureRecognizerStateEnded) {
double score = _secheuristic.scoreOfNextTouch([_touchGestureRecognizer locationInView:self.view]);
if (score > requiredScore)
[self _showPhishingAlert];
}
[self showUI];
}
- (void)_statusBarFrameDidChange:(NSNotificationCenter *)notification
{
CGFloat height = UIApplication.sharedApplication.statusBarFrame.size.height;
if (!height || height == _nonZeroStatusBarHeight)
return;
_nonZeroStatusBarHeight = height;
[self _updateWebViewFullscreenInsets];
}
- (void)_updateWebViewFullscreenInsets
{
if (auto* manager = self._manager)
manager->setFullscreenInsetTop(self._effectiveFullscreenInsetTop);
}
- (void)_showPhishingAlert
{
NSString *alertTitle = WEB_UI_STRING("Deceptive Website Warning", "Fullscreen Deceptive Website Warning Sheet Title");
NSString *alertMessage = [NSString stringWithFormat:WEB_UI_STRING("The website \"%@\" may be a deceptive website. Would you like to exit fullscreen?", "Fullscreen Deceptive Website Warning Sheet Content Text") , (NSString *)self.location];
UIAlertController* alert = [UIAlertController alertControllerWithTitle:alertTitle message:alertMessage preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction* exitAction = [UIAlertAction actionWithTitle:WEB_UI_STRING("Exit Fullscreen", "Fullscreen Deceptive Website Exit Action") style:UIAlertActionStyleCancel handler:^(UIAlertAction * action) {
[self _cancelAction:action];
}];
UIAlertAction* stayAction = [UIAlertAction actionWithTitle:WEB_UI_STRING("Stay in Fullscreen", "Fullscreen Deceptive Website Stay Action") style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) {
_secheuristic.reset();
}];
[alert addAction:exitAction];
[alert addAction:stayAction];
[self presentViewController:alert animated:YES completion:nil];
}
@end
#endif // ENABLE(FULLSCREEN_API) && PLATFORM(IOS)