Fix a few issues in WKFullScreenViewController
[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 <wtf/RetainPtr.h>
40
41 using namespace WebCore;
42 using namespace WebKit;
43
44 static const NSTimeInterval showHideAnimationDuration = 0.1;
45 static const NSTimeInterval autoHideDelay = 4.0;
46 static const double requiredScore = 0.1;
47
48 @class WKFullscreenStackView;
49
50 class WKFullScreenViewControllerPlaybackSessionModelClient : PlaybackSessionModelClient {
51 public:
52     void setParent(WKFullScreenViewController *parent) { m_parent = parent; }
53
54     void rateChanged(bool isPlaying, float) override
55     {
56         m_parent.playing = isPlaying;
57     }
58
59     void pictureInPictureActiveChanged(bool active) override
60     {
61         m_parent.pictureInPictureActive = active;
62     }
63
64     void setInterface(PlaybackSessionInterfaceAVKit* interface)
65     {
66         if (m_interface == interface)
67             return;
68
69         if (m_interface && m_interface->playbackSessionModel())
70             m_interface->playbackSessionModel()->removeClient(*this);
71         m_interface = interface;
72         if (m_interface && m_interface->playbackSessionModel())
73             interface->playbackSessionModel()->addClient(*this);
74     }
75
76 private:
77     WKFullScreenViewController *m_parent { nullptr };
78     RefPtr<PlaybackSessionInterfaceAVKit> m_interface;
79 };
80
81 #pragma mark - _WKExtrinsicButton
82
83 @interface _WKExtrinsicButton : UIButton
84 @property (assign, nonatomic) CGSize extrinsicContentSize;
85 @end
86
87 @implementation _WKExtrinsicButton
88 - (void)setExtrinsicContentSize:(CGSize)size
89 {
90     _extrinsicContentSize = size;
91     [self invalidateIntrinsicContentSize];
92 }
93
94 - (CGSize)intrinsicContentSize
95 {
96     return _extrinsicContentSize;
97 }
98 @end
99
100 #pragma mark - WKFullScreenViewController
101
102 @interface WKFullScreenViewController () <UIGestureRecognizerDelegate, UIToolbarDelegate>
103 @property (weak, nonatomic) WKWebView *_webView; // Cannot be retained, see <rdar://problem/14884666>.
104 @property (readonly, nonatomic) WebFullScreenManagerProxy* _manager;
105 @property (readonly, nonatomic) CGFloat _effectiveFullscreenInsetTop;
106 @end
107
108 @implementation WKFullScreenViewController {
109     RetainPtr<UILongPressGestureRecognizer> _touchGestureRecognizer;
110     RetainPtr<WKFullscreenStackView> _stackView;
111     RetainPtr<_WKExtrinsicButton> _cancelButton;
112     RetainPtr<_WKExtrinsicButton> _pipButton;
113     RetainPtr<UIButton> _locationButton;
114     RetainPtr<UILayoutGuide> _topGuide;
115     RetainPtr<NSLayoutConstraint> _topConstraint;
116     WebKit::FullscreenTouchSecheuristic _secheuristic;
117     WKFullScreenViewControllerPlaybackSessionModelClient _playbackClient;
118     CGFloat _nonZeroStatusBarHeight;
119 }
120
121 #pragma mark - External Interface
122
123 - (id)initWithWebView:(WKWebView *)webView
124 {
125     self = [super init];
126     if (!self)
127         return nil;
128
129     _nonZeroStatusBarHeight = UIApplication.sharedApplication.statusBarFrame.size.height;
130     [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_statusBarFrameDidChange:) name:UIApplicationDidChangeStatusBarFrameNotification object:nil];
131     _secheuristic.setRampUpSpeed(Seconds(0.25));
132     _secheuristic.setRampDownSpeed(Seconds(1.));
133     _secheuristic.setXWeight(0);
134     _secheuristic.setGamma(0.1);
135     _secheuristic.setGammaCutoff(0.08);
136
137     self._webView = webView;
138
139     _playbackClient.setParent(self);
140
141     return self;
142 }
143
144 - (void)dealloc
145 {
146     [NSObject cancelPreviousPerformRequestsWithTarget:self];
147     [[NSNotificationCenter defaultCenter] removeObserver:self];
148
149     _playbackClient.setInterface(nullptr);
150
151     [_target release];
152     [_location release];
153
154     [super dealloc];
155 }
156
157 - (void)showUI
158 {
159     [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(hideUI) object:nil];
160
161     if (_playing) {
162         NSTimeInterval hideDelay = autoHideDelay;
163         [self performSelector:@selector(hideUI) withObject:nil afterDelay:hideDelay];
164     }
165     [UIView animateWithDuration:showHideAnimationDuration animations:^{
166         [_stackView setHidden:NO];
167         [_stackView setAlpha:1];
168         self.prefersStatusBarHidden = NO;
169     }];
170 }
171
172 - (void)hideUI
173 {
174     [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(hideUI) object:nil];
175     [UIView animateWithDuration:showHideAnimationDuration animations:^{
176         [self.view removeConstraints:@[_topConstraint.get()]];
177         _topConstraint = [[_topGuide topAnchor] constraintEqualToAnchor:self.view.safeAreaLayoutGuide.topAnchor constant:self.view.safeAreaInsets.top];
178         [_topConstraint setActive:YES];
179         [_stackView setAlpha:0];
180         self.prefersStatusBarHidden = YES;
181     } completion:^(BOOL finished) {
182         if (!finished)
183             return;
184
185         [self.view removeConstraints:@[_topConstraint.get()]];
186         _topConstraint = [[_topGuide topAnchor] constraintEqualToAnchor:self.view.safeAreaLayoutGuide.topAnchor];
187         [_topConstraint setActive:YES];
188         [_stackView setHidden:YES];
189     }];
190 }
191
192 - (void)videoControlsManagerDidChange
193 {
194     WebPageProxy* page = [self._webView _page];
195     PlaybackSessionManagerProxy* playbackSessionManager = page ? page->playbackSessionManager() : nullptr;
196     PlatformPlaybackSessionInterface* playbackSessionInterface = playbackSessionManager ? playbackSessionManager->controlsManagerInterface() : nullptr;
197     _playbackClient.setInterface(playbackSessionInterface);
198
199     PlaybackSessionModel* playbackSessionModel = playbackSessionInterface ? playbackSessionInterface->playbackSessionModel() : nullptr;
200     self.playing = playbackSessionModel ? playbackSessionModel->isPlaying() : NO;
201     [_pipButton setHidden:!playbackSessionModel];
202 }
203
204 @synthesize prefersStatusBarHidden=_prefersStatusBarHidden;
205
206 - (void)setPrefersStatusBarHidden:(BOOL)value
207 {
208     _prefersStatusBarHidden = value;
209     [self setNeedsStatusBarAppearanceUpdate];
210     [self _updateWebViewFullscreenInsets];
211 }
212
213 - (void)setPlaying:(BOOL)isPlaying
214 {
215     if (_playing == isPlaying)
216         return;
217
218     _playing = isPlaying;
219     if (!_playing)
220         [self showUI];
221     else {
222         [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(hideUI) object:nil];
223         NSTimeInterval hideDelay = autoHideDelay;
224         [self performSelector:@selector(hideUI) withObject:nil afterDelay:hideDelay];
225     }
226 }
227
228 - (void)setPictureInPictureActive:(BOOL)active
229 {
230     if (_pictureInPictureActive == active)
231         return;
232
233     _pictureInPictureActive = active;
234     [_pipButton setSelected:active];
235 }
236
237 #pragma mark - UIViewController Overrides
238
239 - (void)loadView
240 {
241     [self setView:adoptNS([[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)]).get()];
242     self.view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
243
244     _cancelButton = [_WKExtrinsicButton buttonWithType:UIButtonTypeSystem];
245     [_cancelButton setTranslatesAutoresizingMaskIntoConstraints:NO];
246     [_cancelButton setAdjustsImageWhenHighlighted:NO];
247     [_cancelButton setExtrinsicContentSize:CGSizeMake(60.0, 47.0)];
248     [WKFullscreenStackView applyPrimaryGlyphTintToView:_cancelButton.get()];
249     NSBundle *bundle = [NSBundle bundleForClass:self.class];
250     UIImage *doneImage = [UIImage imageNamed:@"Done" inBundle:bundle compatibleWithTraitCollection:nil];
251     [_cancelButton setImage:[doneImage imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate] forState:UIControlStateNormal];
252     [_cancelButton setTintColor:[UIColor whiteColor]];
253     [_cancelButton sizeToFit];
254     [_cancelButton addTarget:self action:@selector(_cancelAction:) forControlEvents:UIControlEventTouchUpInside];
255
256     _pipButton = [_WKExtrinsicButton buttonWithType:UIButtonTypeSystem];
257     [_pipButton setTranslatesAutoresizingMaskIntoConstraints:NO];
258     [_pipButton setAdjustsImageWhenHighlighted:NO];
259     [_pipButton setExtrinsicContentSize:CGSizeMake(60.0, 47.0)];
260     [WKFullscreenStackView applyPrimaryGlyphTintToView:_pipButton.get()];
261     UIImage *startPiPImage = [UIImage imageNamed:@"StartPictureInPictureButton" inBundle:bundle compatibleWithTraitCollection:nil];
262     UIImage *stopPiPImage = [UIImage imageNamed:@"StopPictureInPictureButton" inBundle:bundle compatibleWithTraitCollection:nil];
263     [_pipButton setImage:[startPiPImage imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate] forState:UIControlStateNormal];
264     [_pipButton setImage:[stopPiPImage imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate] forState:UIControlStateSelected];
265     [_pipButton setTintColor:[UIColor whiteColor]];
266     [_pipButton sizeToFit];
267     [_pipButton addTarget:self action:@selector(_togglePiPAction:) forControlEvents:UIControlEventTouchUpInside];
268
269     _stackView = adoptNS([[WKFullscreenStackView alloc] initWithArrangedSubviews:@[_cancelButton.get(), _pipButton.get()] axis:UILayoutConstraintAxisHorizontal]);
270     [_stackView setTranslatesAutoresizingMaskIntoConstraints:NO];
271     [_stackView setTargetViewForSecondaryMaterialOverlay:_cancelButton.get()];
272     [[self view] addSubview:_stackView.get()];
273
274     UILayoutGuide *safeArea = self.view.safeAreaLayoutGuide;
275     UILayoutGuide *margins = self.view.layoutMarginsGuide;
276
277     _topGuide = adoptNS([[UILayoutGuide alloc] init]);
278     [self.view addLayoutGuide:_topGuide.get()];
279     NSLayoutAnchor *topAnchor = [_topGuide topAnchor];
280     _topConstraint = [topAnchor constraintEqualToAnchor:safeArea.topAnchor];
281     [NSLayoutConstraint activateConstraints:@[
282         _topConstraint.get(),
283         [[_stackView topAnchor] constraintEqualToAnchor:topAnchor],
284         [[_stackView leadingAnchor] constraintEqualToAnchor:margins.leadingAnchor],
285     ]];
286
287     [_stackView setAlpha:0];
288     [_stackView setHidden:YES];
289     [self videoControlsManagerDidChange];
290
291     _touchGestureRecognizer = adoptNS([[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(_touchDetected:)]);
292     [_touchGestureRecognizer setCancelsTouchesInView:NO];
293     [_touchGestureRecognizer setMinimumPressDuration:0];
294     [_touchGestureRecognizer setDelegate:self];
295     [self.view addGestureRecognizer:_touchGestureRecognizer.get()];
296 }
297
298 - (void)viewWillAppear:(BOOL)animated
299 {
300     self._webView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
301     self._webView.frame = self.view.bounds;
302     [self.view insertSubview:self._webView atIndex:0];
303
304     if (auto* manager = self._manager)
305         manager->setFullscreenAutoHideDelay(autoHideDelay);
306 }
307
308 - (void)viewDidLayoutSubviews
309 {
310     [self _updateWebViewFullscreenInsets];
311     _secheuristic.setSize(self.view.bounds.size);
312 }
313
314 - (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
315 {
316     [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
317     [coordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
318         [self._webView _beginAnimatedResizeWithUpdates:^{
319             [self._webView _overrideLayoutParametersWithMinimumLayoutSize:size maximumUnobscuredSizeOverride:size];
320         }];
321         [self._webView _setInterfaceOrientationOverride:[UIApp statusBarOrientation]];
322     } completion:^(id <UIViewControllerTransitionCoordinatorContext>context) {
323         [self._webView _endAnimatedResize];
324     }];
325 }
326
327 - (UIStatusBarStyle)preferredStatusBarStyle
328 {
329     return UIStatusBarStyleLightContent;
330 }
331
332 - (BOOL)prefersStatusBarHidden
333 {
334     return _prefersStatusBarHidden;
335 }
336
337 #pragma mark - UIGestureRecognizerDelegate
338
339 - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
340 {
341     return YES;
342 }
343
344 - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
345 {
346     [self showUI];
347     return YES;
348 }
349
350 #pragma mark - Internal Interface
351
352 @dynamic _manager;
353 - (WebFullScreenManagerProxy*)_manager
354 {
355     if (auto* page = [self._webView _page])
356         return page->fullScreenManager();
357     return nullptr;
358 }
359
360 @dynamic _effectiveFullscreenInsetTop;
361 - (CGFloat)_effectiveFullscreenInsetTop
362 {
363     if (self.prefersStatusBarHidden)
364         return 0;
365
366     CGRect cancelFrame = _cancelButton.get().frame;
367     CGPoint maxXY = CGPointMake(CGRectGetMaxX(cancelFrame), CGRectGetMaxY(cancelFrame));
368     return [_cancelButton convertPoint:maxXY toView:self.view].y;
369 }
370
371 - (void)_cancelAction:(id)sender
372 {
373     [[self target] performSelector:[self action]];
374 }
375
376 - (void)_togglePiPAction:(id)sender
377 {
378     WebPageProxy* page = [self._webView _page];
379     if (!page)
380         return;
381
382     PlaybackSessionManagerProxy* playbackSessionManager = page->playbackSessionManager();
383     if (!playbackSessionManager)
384         return;
385
386     PlatformPlaybackSessionInterface* playbackSessionInterface = playbackSessionManager->controlsManagerInterface();
387     if (!playbackSessionInterface)
388         return;
389
390     PlaybackSessionModel* playbackSessionModel = playbackSessionInterface->playbackSessionModel();
391     if (!playbackSessionModel)
392         return;
393
394     playbackSessionModel->togglePictureInPicture();
395 }
396
397 - (void)_touchDetected:(id)sender
398 {
399     if ([_touchGestureRecognizer state] != UIGestureRecognizerStateBegan || [_touchGestureRecognizer state] == UIGestureRecognizerStateEnded) {
400         double score = _secheuristic.scoreOfNextTouch([_touchGestureRecognizer locationInView:self.view]);
401         if (score > requiredScore)
402             [self _showPhishingAlert];
403     }
404     [self showUI];
405 }
406
407 - (void)_statusBarFrameDidChange:(NSNotificationCenter *)notification
408 {
409     CGFloat height = UIApplication.sharedApplication.statusBarFrame.size.height;
410     if (!height || height == _nonZeroStatusBarHeight)
411         return;
412
413     _nonZeroStatusBarHeight = height;
414     [self _updateWebViewFullscreenInsets];
415 }
416
417 - (void)_updateWebViewFullscreenInsets
418 {
419     if (auto* manager = self._manager)
420         manager->setFullscreenInsetTop(self._effectiveFullscreenInsetTop);
421 }
422
423 - (void)_showPhishingAlert
424 {
425     NSString *alertTitle = WEB_UI_STRING("Deceptive Website Warning", "Fullscreen Deceptive Website Warning Sheet Title");
426     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];
427     UIAlertController* alert = [UIAlertController alertControllerWithTitle:alertTitle message:alertMessage preferredStyle:UIAlertControllerStyleAlert];
428
429     UIAlertAction* exitAction = [UIAlertAction actionWithTitle:WEB_UI_STRING("Exit Fullscreen", "Fullscreen Deceptive Website Exit Action") style:UIAlertActionStyleCancel handler:^(UIAlertAction * action) {
430         [self _cancelAction:action];
431     }];
432
433     UIAlertAction* stayAction = [UIAlertAction actionWithTitle:WEB_UI_STRING("Stay in Fullscreen", "Fullscreen Deceptive Website Stay Action") style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) {
434         _secheuristic.reset();
435     }];
436
437     [alert addAction:exitAction];
438     [alert addAction:stayAction];
439     [self presentViewController:alert animated:YES completion:nil];
440 }
441
442 @end
443
444 #endif // ENABLE(FULLSCREEN_API) && PLATFORM(IOS)