02e60eb00498c8052545b6c5b0a99c2e99f52e7a
[WebKit-https.git] / Source / WebKit / UIProcess / ios / fullscreen / WKFullScreenViewController.mm
1 /*
2  * Copyright (C) 2018 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 #import "config.h"
27
28 #if ENABLE(FULLSCREEN_API) && PLATFORM(IOS)
29 #import "WKFullScreenViewController.h"
30
31 #import "FullscreenTouchSecheuristic.h"
32 #import "PlaybackSessionManagerProxy.h"
33 #import "UIKitSPI.h"
34 #import "WKFullscreenStackView.h"
35 #import "WKWebViewInternal.h"
36 #import "WebFullScreenManagerProxy.h"
37 #import "WebPageProxy.h"
38 #import <WebCore/LocalizedStrings.h>
39 #import <pal/spi/cocoa/AVKitSPI.h>
40 #import <wtf/RetainPtr.h>
41
42 using namespace WebCore;
43 using namespace WebKit;
44
45 static const NSTimeInterval showHideAnimationDuration = 0.1;
46 static const NSTimeInterval autoHideDelay = 4.0;
47 static const double requiredScore = 0.1;
48
49 @class WKFullscreenStackView;
50
51 class WKFullScreenViewControllerPlaybackSessionModelClient : PlaybackSessionModelClient {
52 public:
53     void setParent(WKFullScreenViewController *parent) { m_parent = parent; }
54
55     void rateChanged(bool isPlaying, float) override
56     {
57         m_parent.playing = isPlaying;
58     }
59
60     void pictureInPictureActiveChanged(bool active) override
61     {
62         m_parent.pictureInPictureActive = active;
63     }
64
65     void setInterface(PlaybackSessionInterfaceAVKit* interface)
66     {
67         if (m_interface == interface)
68             return;
69
70         if (m_interface && m_interface->playbackSessionModel())
71             m_interface->playbackSessionModel()->removeClient(*this);
72         m_interface = interface;
73         if (m_interface && m_interface->playbackSessionModel())
74             m_interface->playbackSessionModel()->addClient(*this);
75     }
76
77 private:
78     WKFullScreenViewController *m_parent { nullptr };
79     RefPtr<PlaybackSessionInterfaceAVKit> m_interface;
80 };
81
82 #pragma mark - _WKExtrinsicButton
83
84 @interface _WKExtrinsicButton : UIButton
85 @property (assign, nonatomic) CGSize extrinsicContentSize;
86 @end
87
88 @implementation _WKExtrinsicButton
89 - (void)setExtrinsicContentSize:(CGSize)size
90 {
91     _extrinsicContentSize = size;
92     [self invalidateIntrinsicContentSize];
93 }
94
95 - (CGSize)intrinsicContentSize
96 {
97     return _extrinsicContentSize;
98 }
99 @end
100
101 #pragma mark - WKFullScreenViewController
102
103 @interface WKFullScreenViewController () <UIGestureRecognizerDelegate, UIToolbarDelegate>
104 @property (weak, nonatomic) WKWebView *_webView; // Cannot be retained, see <rdar://problem/14884666>.
105 @property (readonly, nonatomic) WebFullScreenManagerProxy* _manager;
106 @property (readonly, nonatomic) WebCore::FloatBoxExtent _effectiveFullscreenInsets;
107 @end
108
109 @implementation WKFullScreenViewController {
110     RetainPtr<UILongPressGestureRecognizer> _touchGestureRecognizer;
111     RetainPtr<WKFullscreenStackView> _stackView;
112     RetainPtr<_WKExtrinsicButton> _cancelButton;
113     RetainPtr<_WKExtrinsicButton> _pipButton;
114     RetainPtr<UIButton> _locationButton;
115     RetainPtr<UILayoutGuide> _topGuide;
116     RetainPtr<NSLayoutConstraint> _topConstraint;
117     WebKit::FullscreenTouchSecheuristic _secheuristic;
118     WKFullScreenViewControllerPlaybackSessionModelClient _playbackClient;
119     CGFloat _nonZeroStatusBarHeight;
120 }
121
122 @synthesize prefersStatusBarHidden=_prefersStatusBarHidden;
123 @synthesize prefersHomeIndicatorAutoHidden=_prefersHomeIndicatorAutoHidden;
124
125 #pragma mark - External Interface
126
127 - (id)initWithWebView:(WKWebView *)webView
128 {
129     self = [super init];
130     if (!self)
131         return nil;
132
133     _nonZeroStatusBarHeight = UIApplication.sharedApplication.statusBarFrame.size.height;
134     [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_statusBarFrameDidChange:) name:UIApplicationDidChangeStatusBarFrameNotification object:nil];
135     _secheuristic.setRampUpSpeed(Seconds(0.25));
136     _secheuristic.setRampDownSpeed(Seconds(1.));
137     _secheuristic.setXWeight(0);
138     _secheuristic.setGamma(0.1);
139     _secheuristic.setGammaCutoff(0.08);
140
141     self._webView = webView;
142
143     _playbackClient.setParent(self);
144
145     return self;
146 }
147
148 - (void)dealloc
149 {
150     [NSObject cancelPreviousPerformRequestsWithTarget:self];
151     [[NSNotificationCenter defaultCenter] removeObserver:self];
152
153     _playbackClient.setInterface(nullptr);
154
155     [_target release];
156     [_location release];
157
158     [super dealloc];
159 }
160
161 - (void)showUI
162 {
163     [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(hideUI) object:nil];
164
165     if (_playing) {
166         NSTimeInterval hideDelay = autoHideDelay;
167         [self performSelector:@selector(hideUI) withObject:nil afterDelay:hideDelay];
168     }
169     [UIView animateWithDuration:showHideAnimationDuration animations:^{
170         [_stackView setHidden:NO];
171         [_stackView setAlpha:1];
172         self.prefersStatusBarHidden = NO;
173         self.prefersHomeIndicatorAutoHidden = NO;
174         if (_topConstraint)
175             [NSLayoutConstraint deactivateConstraints:@[_topConstraint.get()]];
176         _topConstraint = [[_topGuide topAnchor] constraintEqualToAnchor:self.view.safeAreaLayoutGuide.topAnchor];
177         [_topConstraint setActive:YES];
178         if (auto* manager = self._manager)
179             manager->setFullscreenControlsHidden(false);
180     }];
181 }
182
183 - (void)hideUI
184 {
185     [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(hideUI) object:nil];
186     [UIView animateWithDuration:showHideAnimationDuration animations:^{
187
188         if (_topConstraint)
189             [NSLayoutConstraint deactivateConstraints:@[_topConstraint.get()]];
190         _topConstraint = [[_topGuide topAnchor] constraintEqualToAnchor:self.view.topAnchor constant:self.view.safeAreaInsets.top];
191         [_topConstraint setActive:YES];
192         [_stackView setAlpha:0];
193         self.prefersStatusBarHidden = YES;
194         self.prefersHomeIndicatorAutoHidden = YES;
195         if (auto* manager = self._manager)
196             manager->setFullscreenControlsHidden(true);
197     } completion:^(BOOL finished) {
198         if (!finished)
199             return;
200
201         [_stackView setHidden:YES];
202     }];
203 }
204
205 - (void)videoControlsManagerDidChange
206 {
207     WebPageProxy* page = [self._webView _page];
208     PlaybackSessionManagerProxy* playbackSessionManager = page ? page->playbackSessionManager() : nullptr;
209     PlatformPlaybackSessionInterface* playbackSessionInterface = playbackSessionManager ? playbackSessionManager->controlsManagerInterface() : nullptr;
210     _playbackClient.setInterface(playbackSessionInterface);
211
212     PlaybackSessionModel* playbackSessionModel = playbackSessionInterface ? playbackSessionInterface->playbackSessionModel() : nullptr;
213     self.playing = playbackSessionModel ? playbackSessionModel->isPlaying() : NO;
214     [_pipButton setHidden:!playbackSessionModel];
215 }
216
217 - (void)setPrefersStatusBarHidden:(BOOL)value
218 {
219     _prefersStatusBarHidden = value;
220     [self setNeedsStatusBarAppearanceUpdate];
221     [self _updateWebViewFullscreenInsets];
222 }
223
224 - (void)setPrefersHomeIndicatorAutoHidden:(BOOL)value
225 {
226     _prefersHomeIndicatorAutoHidden = value;
227     [self setNeedsUpdateOfHomeIndicatorAutoHidden];
228 }
229
230 - (void)setPlaying:(BOOL)isPlaying
231 {
232     if (_playing == isPlaying)
233         return;
234
235     _playing = isPlaying;
236     if (!_playing)
237         [self showUI];
238     else {
239         [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(hideUI) object:nil];
240         NSTimeInterval hideDelay = autoHideDelay;
241         [self performSelector:@selector(hideUI) withObject:nil afterDelay:hideDelay];
242     }
243 }
244
245 - (void)setPictureInPictureActive:(BOOL)active
246 {
247     if (_pictureInPictureActive == active)
248         return;
249
250     _pictureInPictureActive = active;
251     [_pipButton setSelected:active];
252 }
253
254 - (void)setAnimating:(BOOL)animating
255 {
256     if (_animating == animating)
257         return;
258     _animating = animating;
259     [self setNeedsStatusBarAppearanceUpdate];
260
261     if (_animating)
262         [self hideUI];
263     else
264         [self showUI];
265 }
266
267 #pragma mark - UIViewController Overrides
268
269 - (void)loadView
270 {
271     [self setView:adoptNS([[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)]).get()];
272     self.view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
273
274     _cancelButton = [_WKExtrinsicButton buttonWithType:UIButtonTypeSystem];
275     [_cancelButton setTranslatesAutoresizingMaskIntoConstraints:NO];
276     [_cancelButton setAdjustsImageWhenHighlighted:NO];
277     [_cancelButton setExtrinsicContentSize:CGSizeMake(60.0, 47.0)];
278     NSBundle *bundle = [NSBundle bundleForClass:self.class];
279     UIImage *doneImage = [UIImage imageNamed:@"Done" inBundle:bundle compatibleWithTraitCollection:nil];
280     [_cancelButton setImage:[doneImage imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate] forState:UIControlStateNormal];
281     [_cancelButton setTintColor:[UIColor whiteColor]];
282     [_cancelButton sizeToFit];
283     [_cancelButton addTarget:self action:@selector(_cancelAction:) forControlEvents:UIControlEventTouchUpInside];
284
285     _pipButton = [_WKExtrinsicButton buttonWithType:UIButtonTypeSystem];
286     [_pipButton setTranslatesAutoresizingMaskIntoConstraints:NO];
287     [_pipButton setAdjustsImageWhenHighlighted:NO];
288     [_pipButton setExtrinsicContentSize:CGSizeMake(60.0, 47.0)];
289     UIImage *startPiPImage = [UIImage imageNamed:@"StartPictureInPictureButton" inBundle:bundle compatibleWithTraitCollection:nil];
290     UIImage *stopPiPImage = [UIImage imageNamed:@"StopPictureInPictureButton" inBundle:bundle compatibleWithTraitCollection:nil];
291     [_pipButton setImage:[startPiPImage imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate] forState:UIControlStateNormal];
292     [_pipButton setImage:[stopPiPImage imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate] forState:UIControlStateSelected];
293     [_pipButton setTintColor:[UIColor whiteColor]];
294     [_pipButton sizeToFit];
295     [_pipButton addTarget:self action:@selector(_togglePiPAction:) forControlEvents:UIControlEventTouchUpInside];
296
297     _stackView = adoptNS([[WKFullscreenStackView alloc] init]);
298     [_stackView setTranslatesAutoresizingMaskIntoConstraints:NO];
299     [_stackView addArrangedSubview:_cancelButton.get() applyingMaterialStyle:AVBackgroundViewMaterialStyleSecondary tintEffectStyle:AVBackgroundViewTintEffectStyleSecondary];
300     [_stackView addArrangedSubview:_pipButton.get() applyingMaterialStyle:AVBackgroundViewMaterialStylePrimary tintEffectStyle:AVBackgroundViewTintEffectStyleSecondary];
301     [[self view] addSubview:_stackView.get()];
302
303     UILayoutGuide *safeArea = self.view.safeAreaLayoutGuide;
304     UILayoutGuide *margins = self.view.layoutMarginsGuide;
305
306     _topGuide = adoptNS([[UILayoutGuide alloc] init]);
307     [self.view addLayoutGuide:_topGuide.get()];
308     NSLayoutAnchor *topAnchor = [_topGuide topAnchor];
309     _topConstraint = [topAnchor constraintEqualToAnchor:safeArea.topAnchor];
310     [NSLayoutConstraint activateConstraints:@[
311         _topConstraint.get(),
312         [[_stackView topAnchor] constraintEqualToAnchor:topAnchor],
313         [[_stackView leadingAnchor] constraintEqualToAnchor:margins.leadingAnchor],
314     ]];
315
316     [_stackView setAlpha:0];
317     [_stackView setHidden:YES];
318     [self videoControlsManagerDidChange];
319
320     _touchGestureRecognizer = adoptNS([[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(_touchDetected:)]);
321     [_touchGestureRecognizer setCancelsTouchesInView:NO];
322     [_touchGestureRecognizer setMinimumPressDuration:0];
323     [_touchGestureRecognizer setDelegate:self];
324     [self.view addGestureRecognizer:_touchGestureRecognizer.get()];
325 }
326
327 - (void)viewWillAppear:(BOOL)animated
328 {
329     self._webView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
330     self._webView.frame = self.view.bounds;
331     [self.view insertSubview:self._webView atIndex:0];
332
333     if (auto* manager = self._manager)
334         manager->setFullscreenAutoHideDuration(Seconds(showHideAnimationDuration));
335
336     [super viewWillAppear:animated];
337 }
338
339 - (void)viewDidLayoutSubviews
340 {
341     [self _updateWebViewFullscreenInsets];
342     _secheuristic.setSize(self.view.bounds.size);
343 }
344
345 - (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
346 {
347     [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
348     [coordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
349         [self._webView _beginAnimatedResizeWithUpdates:^{
350             [self._webView _overrideLayoutParametersWithMinimumLayoutSize:size maximumUnobscuredSizeOverride:size];
351         }];
352         [self._webView _setInterfaceOrientationOverride:[UIApp statusBarOrientation]];
353     } completion:^(id <UIViewControllerTransitionCoordinatorContext>context) {
354         [self._webView _endAnimatedResize];
355     }];
356 }
357
358 - (UIStatusBarStyle)preferredStatusBarStyle
359 {
360     return UIStatusBarStyleLightContent;
361 }
362
363 - (BOOL)prefersStatusBarHidden
364 {
365     return _animating || _prefersStatusBarHidden;
366 }
367
368 #pragma mark - UIGestureRecognizerDelegate
369
370 - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
371 {
372     return YES;
373 }
374
375 - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
376 {
377     if (!self.animating)
378         [self showUI];
379     return YES;
380 }
381
382 #pragma mark - Internal Interface
383
384 @dynamic _manager;
385 - (WebFullScreenManagerProxy*)_manager
386 {
387     if (auto* page = [self._webView _page])
388         return page->fullScreenManager();
389     return nullptr;
390 }
391
392 @dynamic _effectiveFullscreenInsets;
393 - (WebCore::FloatBoxExtent)_effectiveFullscreenInsets
394 {
395     auto safeAreaInsets = self.view.safeAreaInsets;
396     WebCore::FloatBoxExtent insets { safeAreaInsets.top, safeAreaInsets.right, safeAreaInsets.bottom, safeAreaInsets.left };
397
398     CGRect cancelFrame = _cancelButton.get().frame;
399     CGPoint maxXY = CGPointMake(CGRectGetMaxX(cancelFrame), CGRectGetMaxY(cancelFrame));
400     insets.setTop([_cancelButton convertPoint:maxXY toView:self.view].y);
401     return insets;
402 }
403
404 - (void)_cancelAction:(id)sender
405 {
406     [[self target] performSelector:[self action]];
407 }
408
409 - (void)_togglePiPAction:(id)sender
410 {
411     WebPageProxy* page = [self._webView _page];
412     if (!page)
413         return;
414
415     PlaybackSessionManagerProxy* playbackSessionManager = page->playbackSessionManager();
416     if (!playbackSessionManager)
417         return;
418
419     PlatformPlaybackSessionInterface* playbackSessionInterface = playbackSessionManager->controlsManagerInterface();
420     if (!playbackSessionInterface)
421         return;
422
423     PlaybackSessionModel* playbackSessionModel = playbackSessionInterface->playbackSessionModel();
424     if (!playbackSessionModel)
425         return;
426
427     playbackSessionModel->togglePictureInPicture();
428 }
429
430 - (void)_touchDetected:(id)sender
431 {
432     if ([_touchGestureRecognizer state] == UIGestureRecognizerStateBegan || [_touchGestureRecognizer state] == UIGestureRecognizerStateEnded) {
433         double score = _secheuristic.scoreOfNextTouch([_touchGestureRecognizer locationInView:self.view]);
434         if (score > requiredScore)
435             [self _showPhishingAlert];
436     }
437     if (!self.animating)
438         [self showUI];
439 }
440
441 - (void)_statusBarFrameDidChange:(NSNotificationCenter *)notification
442 {
443     CGFloat height = UIApplication.sharedApplication.statusBarFrame.size.height;
444     if (!height || height == _nonZeroStatusBarHeight)
445         return;
446
447     _nonZeroStatusBarHeight = height;
448     [self _updateWebViewFullscreenInsets];
449 }
450
451 - (void)_updateWebViewFullscreenInsets
452 {
453     if (auto* manager = self._manager)
454         manager->setFullscreenInsets(self._effectiveFullscreenInsets);
455 }
456
457 - (void)_showPhishingAlert
458 {
459     NSString *alertTitle = [NSString stringWithFormat:WEB_UI_STRING("It looks like you are typing on “%@”", "Fullscreen Deceptive Website Warning Sheet Title"), (NSString *)self.location];
460     NSString *alertMessage = WEB_UI_STRING("Typing is not allowed in full screen. This website may be showing a fake keyboard to trick you into disclosing personal or financial information.", "Fullscreen Deceptive Website Warning Sheet Content Text");
461     UIAlertController* alert = [UIAlertController alertControllerWithTitle:alertTitle message:alertMessage preferredStyle:UIAlertControllerStyleAlert];
462
463     if (auto* page = [self._webView _page])
464         page->suspendActiveDOMObjectsAndAnimations();
465
466     UIAlertAction* exitAction = [UIAlertAction actionWithTitle:WEB_UI_STRING_KEY("Exit Full Screen", "Exit Full Screen (Element Fullscreen)", "Fullscreen Deceptive Website Exit Action") style:UIAlertActionStyleCancel handler:^(UIAlertAction * action) {
467         [self _cancelAction:action];
468         if (auto* page = [self._webView _page])
469             page->resumeActiveDOMObjectsAndAnimations();
470     }];
471
472     UIAlertAction* stayAction = [UIAlertAction actionWithTitle:WEB_UI_STRING_KEY("Stay in Full Screen", "Stay in Full Screen (Element Fullscreen)", "Fullscreen Deceptive Website Stay Action") style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) {
473         if (auto* page = [self._webView _page])
474             page->resumeActiveDOMObjectsAndAnimations();
475         _secheuristic.reset();
476     }];
477
478     [alert addAction:exitAction];
479     [alert addAction:stayAction];
480     [self presentViewController:alert animated:YES completion:nil];
481 }
482
483 @end
484
485 #endif // ENABLE(FULLSCREEN_API) && PLATFORM(IOS)