58d09e5f44a77cc5a0e5c735e247c452f8505b76
[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             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 (assign, 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     [super dealloc];
152 }
153
154 - (void)showUI
155 {
156     [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(hideUI) object:nil];
157
158     if (_playing) {
159         NSTimeInterval hideDelay = autoHideDelay;
160         [self performSelector:@selector(hideUI) withObject:nil afterDelay:hideDelay];
161     }
162     [UIView animateWithDuration:showHideAnimationDuration animations:^{
163         [_stackView setHidden:NO];
164         [_stackView setAlpha:1];
165         self.prefersStatusBarHidden = NO;
166     }];
167 }
168
169 - (void)hideUI
170 {
171     [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(hideUI) object:nil];
172     [UIView animateWithDuration:showHideAnimationDuration animations:^{
173         [self.view removeConstraints:@[_topConstraint.get()]];
174         _topConstraint = [[_topGuide topAnchor] constraintEqualToAnchor:self.view.safeAreaLayoutGuide.topAnchor constant:self.view.safeAreaInsets.top];
175         [_topConstraint setActive:YES];
176         [_stackView setAlpha:0];
177         self.prefersStatusBarHidden = YES;
178     } completion:^(BOOL finished) {
179         if (!finished)
180             return;
181
182         [self.view removeConstraints:@[_topConstraint.get()]];
183         _topConstraint = [[_topGuide topAnchor] constraintEqualToAnchor:self.view.safeAreaLayoutGuide.topAnchor];
184         [_topConstraint setActive:YES];
185         [_stackView setHidden:YES];
186     }];
187 }
188
189 - (void)videoControlsManagerDidChange
190 {
191     WebPageProxy* page = [self._webView _page];
192     PlaybackSessionManagerProxy* playbackSessionManager = page ? page->playbackSessionManager() : nullptr;
193     PlatformPlaybackSessionInterface* playbackSessionInterface = playbackSessionManager ? playbackSessionManager->controlsManagerInterface() : nullptr;
194     _playbackClient.setInterface(playbackSessionInterface);
195
196     PlaybackSessionModel* playbackSessionModel = playbackSessionInterface ? playbackSessionInterface->playbackSessionModel() : nullptr;
197     self.playing = playbackSessionModel ? playbackSessionModel->isPlaying() : NO;
198     [_pipButton setHidden:!playbackSessionModel];
199 }
200
201 @synthesize prefersStatusBarHidden=_prefersStatusBarHidden;
202
203 - (void)setPrefersStatusBarHidden:(BOOL)value
204 {
205     _prefersStatusBarHidden = value;
206     [self setNeedsStatusBarAppearanceUpdate];
207     [self _updateWebViewFullscreenInsets];
208 }
209
210 - (void)setPlaying:(BOOL)isPlaying
211 {
212     if (_playing == isPlaying)
213         return;
214
215     _playing = isPlaying;
216     if (!_playing)
217         [self showUI];
218     else {
219         [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(hideUI) object:nil];
220         NSTimeInterval hideDelay = autoHideDelay;
221         [self performSelector:@selector(hideUI) withObject:nil afterDelay:hideDelay];
222     }
223 }
224
225 - (void)setPictureInPictureActive:(BOOL)active
226 {
227     if (_pictureInPictureActive == active)
228         return;
229
230     _pictureInPictureActive = active;
231     [_pipButton setSelected:active];
232 }
233
234 #pragma mark - UIViewController Overrides
235
236 - (void)loadView
237 {
238     [self setView:adoptNS([[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)]).get()];
239     self.view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
240
241     _cancelButton = [_WKExtrinsicButton buttonWithType:UIButtonTypeSystem];
242     [_cancelButton setTranslatesAutoresizingMaskIntoConstraints:NO];
243     [_cancelButton setAdjustsImageWhenHighlighted:NO];
244     [_cancelButton setExtrinsicContentSize:CGSizeMake(60.0, 47.0)];
245     [WKFullscreenStackView applyPrimaryGlyphTintToView:_cancelButton.get()];
246     NSBundle *bundle = [NSBundle bundleForClass:self.class];
247     UIImage *doneImage = [UIImage imageNamed:@"Done" inBundle:bundle compatibleWithTraitCollection:nil];
248     [_cancelButton setImage:[doneImage imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate] forState:UIControlStateNormal];
249     [_cancelButton setTintColor:[UIColor whiteColor]];
250     [_cancelButton sizeToFit];
251     [_cancelButton addTarget:self action:@selector(_cancelAction:) forControlEvents:UIControlEventTouchUpInside];
252
253     _pipButton = [_WKExtrinsicButton buttonWithType:UIButtonTypeSystem];
254     [_pipButton setTranslatesAutoresizingMaskIntoConstraints:NO];
255     [_pipButton setAdjustsImageWhenHighlighted:NO];
256     [_pipButton setExtrinsicContentSize:CGSizeMake(60.0, 47.0)];
257     [WKFullscreenStackView applyPrimaryGlyphTintToView:_pipButton.get()];
258     UIImage *startPiPImage = [UIImage imageNamed:@"StartPictureInPictureButton" inBundle:bundle compatibleWithTraitCollection:nil];
259     UIImage *stopPiPImage = [UIImage imageNamed:@"StopPictureInPictureButton" inBundle:bundle compatibleWithTraitCollection:nil];
260     [_pipButton setImage:[startPiPImage imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate] forState:UIControlStateNormal];
261     [_pipButton setImage:[stopPiPImage imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate] forState:UIControlStateSelected];
262     [_pipButton setTintColor:[UIColor whiteColor]];
263     [_pipButton sizeToFit];
264     [_pipButton addTarget:self action:@selector(_togglePiPAction:) forControlEvents:UIControlEventTouchUpInside];
265
266     _stackView = adoptNS([[WKFullscreenStackView alloc] initWithArrangedSubviews:@[_cancelButton.get(), _pipButton.get()] axis:UILayoutConstraintAxisHorizontal]);
267     [_stackView setTranslatesAutoresizingMaskIntoConstraints:NO];
268     [_stackView setTargetViewForSecondaryMaterialOverlay:_cancelButton.get()];
269     [[self view] addSubview:_stackView.get()];
270
271     UILayoutGuide *safeArea = self.view.safeAreaLayoutGuide;
272     UILayoutGuide *margins = self.view.layoutMarginsGuide;
273
274     _topGuide = adoptNS([[UILayoutGuide alloc] init]);
275     [self.view addLayoutGuide:_topGuide.get()];
276     NSLayoutAnchor *topAnchor = [_topGuide topAnchor];
277     _topConstraint = [topAnchor constraintEqualToAnchor:safeArea.topAnchor];
278     [NSLayoutConstraint activateConstraints:@[
279         _topConstraint.get(),
280         [[_stackView topAnchor] constraintEqualToAnchor:topAnchor],
281         [[_stackView leadingAnchor] constraintEqualToAnchor:margins.leadingAnchor],
282     ]];
283
284     [_stackView setAlpha:0];
285     [_stackView setHidden:YES];
286     [self videoControlsManagerDidChange];
287
288     _touchGestureRecognizer = adoptNS([[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(_touchDetected:)]);
289     [_touchGestureRecognizer setCancelsTouchesInView:NO];
290     [_touchGestureRecognizer setMinimumPressDuration:0];
291     [_touchGestureRecognizer setDelegate:self];
292     [self.view addGestureRecognizer:_touchGestureRecognizer.get()];
293 }
294
295 - (void)viewWillAppear:(BOOL)animated
296 {
297     self._webView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
298     self._webView.frame = self.view.bounds;
299     [self.view insertSubview:self._webView atIndex:0];
300
301     if (auto* manager = self._manager)
302         manager->setFullscreenAutoHideDelay(autoHideDelay);
303 }
304
305 - (void)viewDidLayoutSubviews
306 {
307     [self _updateWebViewFullscreenInsets];
308     _secheuristic.setSize(self.view.bounds.size);
309 }
310
311 - (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
312 {
313     [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
314     [coordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
315         [self._webView _beginAnimatedResizeWithUpdates:^{
316             [self._webView _overrideLayoutParametersWithMinimumLayoutSize:size maximumUnobscuredSizeOverride:size];
317         }];
318         [self._webView _setInterfaceOrientationOverride:[UIApp statusBarOrientation]];
319     } completion:^(id <UIViewControllerTransitionCoordinatorContext>context) {
320         [self._webView _endAnimatedResize];
321     }];
322 }
323
324 - (UIStatusBarStyle)preferredStatusBarStyle
325 {
326     return UIStatusBarStyleLightContent;
327 }
328
329 - (BOOL)prefersStatusBarHidden
330 {
331     return _prefersStatusBarHidden;
332 }
333
334 #pragma mark - UIGestureRecognizerDelegate
335
336 - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
337 {
338     return YES;
339 }
340
341 - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
342 {
343     [self showUI];
344     return YES;
345 }
346
347 #pragma mark - Internal Interface
348
349 @dynamic _manager;
350 - (WebFullScreenManagerProxy*)_manager
351 {
352     if (auto* page = [self._webView _page])
353         return page->fullScreenManager();
354     return nullptr;
355 }
356
357 @dynamic _effectiveFullscreenInsetTop;
358 - (CGFloat)_effectiveFullscreenInsetTop
359 {
360     if (!self.prefersStatusBarHidden)
361         return 0;
362
363     CGRect cancelFrame = _cancelButton.get().frame;
364     CGPoint maxXY = CGPointMake(CGRectGetMaxX(cancelFrame), CGRectGetMaxY(cancelFrame));
365     return [_cancelButton convertPoint:maxXY toView:self.view].y;
366 }
367
368 - (void)_cancelAction:(id)sender
369 {
370     [[self target] performSelector:[self action]];
371 }
372
373 - (void)_togglePiPAction:(id)sender
374 {
375     WebPageProxy* page = [self._webView _page];
376     if (!page)
377         return;
378
379     PlaybackSessionManagerProxy* playbackSessionManager = page->playbackSessionManager();
380     if (!playbackSessionManager)
381         return;
382
383     PlatformPlaybackSessionInterface* playbackSessionInterface = playbackSessionManager->controlsManagerInterface();
384     if (!playbackSessionInterface)
385         return;
386
387     PlaybackSessionModel* playbackSessionModel = playbackSessionInterface->playbackSessionModel();
388     if (!playbackSessionModel)
389         return;
390
391     playbackSessionModel->togglePictureInPicture();
392 }
393
394 - (void)_touchDetected:(id)sender
395 {
396     if ([_touchGestureRecognizer state] != UIGestureRecognizerStateBegan || [_touchGestureRecognizer state] == UIGestureRecognizerStateEnded) {
397         double score = _secheuristic.scoreOfNextTouch([_touchGestureRecognizer locationInView:self.view]);
398         if (score > requiredScore)
399             [self _showPhishingAlert];
400     }
401     [self showUI];
402 }
403
404 - (void)_statusBarFrameDidChange:(NSNotificationCenter *)notification
405 {
406     CGFloat height = UIApplication.sharedApplication.statusBarFrame.size.height;
407     if (!height || height == _nonZeroStatusBarHeight)
408         return;
409
410     _nonZeroStatusBarHeight = height;
411     [self _updateWebViewFullscreenInsets];
412 }
413
414 - (void)_updateWebViewFullscreenInsets
415 {
416     if (auto* manager = self._manager)
417         manager->setFullscreenInsetTop(self._effectiveFullscreenInsetTop);
418 }
419
420 - (void)_showPhishingAlert
421 {
422     NSString *alertTitle = WEB_UI_STRING("Deceptive Website Warning", "Fullscreen Deceptive Website Warning Sheet Title");
423     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];
424     UIAlertController* alert = [UIAlertController alertControllerWithTitle:alertTitle message:alertMessage preferredStyle:UIAlertControllerStyleAlert];
425
426     UIAlertAction* exitAction = [UIAlertAction actionWithTitle:WEB_UI_STRING("Exit Fullscreen", "Fullscreen Deceptive Website Exit Action") style:UIAlertActionStyleCancel handler:^(UIAlertAction * action) {
427         [self _cancelAction:action];
428     }];
429
430     UIAlertAction* stayAction = [UIAlertAction actionWithTitle:WEB_UI_STRING("Stay in Fullscreen", "Fullscreen Deceptive Website Stay Action") style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) {
431         _secheuristic.reset();
432     }];
433
434     [alert addAction:exitAction];
435     [alert addAction:stayAction];
436     [self presentViewController:alert animated:YES completion:nil];
437 }
438
439 @end
440
441 #endif // ENABLE(FULLSCREEN_API) && PLATFORM(IOS)