1/*
2 * Copyright (C) 2008, 2009, 2010, 2012, 2014 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. ``AS IS'' AND ANY
14 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
17 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
18 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
19 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
20 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
21 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24 */
25
26#if PLATFORM(IOS)
27
28#import "WebGeolocationProviderIOS.h"
29
30#import "WebGeolocationCoreLocationProvider.h"
31#import <WebGeolocationPosition.h>
32#import <WebCore/GeolocationPosition.h>
33#import <WebCore/WebCoreThread.h>
34#import <WebCore/WebCoreThreadRun.h>
35#import <wtf/HashSet.h>
36#import <wtf/HashMap.h>
37#import <wtf/RetainPtr.h>
38#import <wtf/Vector.h>
39
40#if PLATFORM(IOS)
41#import "WebDelegateImplementationCaching.h"
42#endif
43
44using namespace WebCore;
45
46@interface WebGeolocationPosition (Internal)
47- (id)initWithGeolocationPosition:(PassRefPtr<GeolocationPosition>)coreGeolocationPosition;
48@end
49
50// CoreLocation runs in the main thread. WebGeolocationProviderIOS lives on the WebThread.
51// _WebCoreLocationUpdateThreadingProxy forward updates from CoreLocation to WebGeolocationProviderIOS.
52@interface _WebCoreLocationUpdateThreadingProxy : NSObject<WebGeolocationCoreLocationUpdateListener>
53- (id)initWithProvider:(WebGeolocationProviderIOS*)provider;
54@end
55
56typedef HashMap<RetainPtr<WebView>, RetainPtr<id<WebGeolocationProviderInitializationListener> > > GeolocationInitializationCallbackMap;
57
58@implementation WebGeolocationProviderIOS {
59@private
60    RetainPtr<WebGeolocationCoreLocationProvider> _coreLocationProvider;
61    RetainPtr<_WebCoreLocationUpdateThreadingProxy> _coreLocationUpdateListenerProxy;
62
63    BOOL _enableHighAccuracy;
64    BOOL _isSuspended;
65    BOOL _shouldResetOnResume;
66
67    // WebViews waiting for CoreLocation to be ready. If the Application does not yet have the permission to use Geolocation
68    // we also have to wait for that to be granted.
69    GeolocationInitializationCallbackMap _webViewsWaitingForCoreLocationAuthorization;
70
71    // List of WebView needing the initial position after registerWebView:. This is needed because WebKit does not
72    // handle sending the position synchronously in response to registerWebView:, so we queue sending lastPosition behind a timer.
73    HashSet<WebView*> _pendingInitialPositionWebView;
74
75    // List of WebViews registered to WebGeolocationProvider for Geolocation update.
76    HashSet<WebView*> _registeredWebViews;
77
78    // All the views that might need a reset if the permission change externally.
79    HashSet<WebView*> _trackedWebViews;
80
81    RetainPtr<NSTimer> _sendLastPositionAsynchronouslyTimer;
82    RetainPtr<WebGeolocationPosition> _lastPosition;
83}
84
85static inline void abortSendLastPosition(WebGeolocationProviderIOS* provider)
86{
87    provider->_pendingInitialPositionWebView.clear();
88    [provider->_sendLastPositionAsynchronouslyTimer.get() invalidate];
89    provider->_sendLastPositionAsynchronouslyTimer.clear();
90}
91
92- (void)dealloc
93{
94    abortSendLastPosition(self);
95    [super dealloc];
96}
97
98#pragma mark - Public API of WebGeolocationProviderIOS.
99+ (WebGeolocationProviderIOS *)sharedGeolocationProvider
100{
101    static dispatch_once_t once;
102    static WebGeolocationProviderIOS *sharedGeolocationProvider;
103    dispatch_once(&once, ^{
104        sharedGeolocationProvider = [[WebGeolocationProviderIOS alloc] init];
105    });
106    return sharedGeolocationProvider;
107}
108
109- (void)suspend
110{
111    ASSERT(WebThreadIsLockedOrDisabled());
112    ASSERT(pthread_main_np());
113
114    ASSERT(!_isSuspended);
115    _isSuspended = YES;
116
117    // A new position is acquired and sent to all registered views on resume.
118    _lastPosition.clear();
119    abortSendLastPosition(self);
120    [_coreLocationProvider stop];
121}
122
123- (void)resume
124{
125    ASSERT(WebThreadIsLockedOrDisabled());
126    ASSERT(pthread_main_np());
127
128    ASSERT(_isSuspended);
129    _isSuspended = NO;
130
131    if (_shouldResetOnResume) {
132        [self resetGeolocation];
133        _shouldResetOnResume = NO;
134        return;
135    }
136
137    if (_registeredWebViews.isEmpty() && _webViewsWaitingForCoreLocationAuthorization.isEmpty())
138        return;
139
140    if (!_coreLocationProvider) {
141        ASSERT(!_coreLocationUpdateListenerProxy);
142        _coreLocationUpdateListenerProxy = adoptNS([[_WebCoreLocationUpdateThreadingProxy alloc] initWithProvider:self]);
143        _coreLocationProvider = adoptNS([[WebGeolocationCoreLocationProvider alloc] initWithListener:_coreLocationUpdateListenerProxy.get()]);
144    }
145
146    if (!_webViewsWaitingForCoreLocationAuthorization.isEmpty())
147        [_coreLocationProvider requestGeolocationAuthorization];
148
149    if (!_registeredWebViews.isEmpty()) {
150        [_coreLocationProvider setEnableHighAccuracy:_enableHighAccuracy];
151        [_coreLocationProvider start];
152    }
153}
154
155#pragma mark - Internal utility methods
156
157- (void)_handlePendingInitialPosition:(NSTimer*)timer
158{
159    ASSERT_UNUSED(timer, timer == _sendLastPositionAsynchronouslyTimer);
160
161    if (_lastPosition) {
162        Vector<WebView*> webViewsCopy;
163        copyToVector(_pendingInitialPositionWebView, webViewsCopy);
164        for (size_t i = 0; i < webViewsCopy.size(); ++i)
165            [webViewsCopy[i] _geolocationDidChangePosition:_lastPosition.get()];
166    }
167    abortSendLastPosition(self);
168}
169
170#pragma mark - Implementation of WebGeolocationProvider
171
172- (void)registerWebView:(WebView *)webView
173{
174    ASSERT(WebThreadIsLockedOrDisabled());
175
176    if (_registeredWebViews.contains(webView))
177        return;
178
179    _registeredWebViews.add(webView);
180#if PLATFORM(IOS)
181    if (!CallUIDelegateReturningBoolean(YES, webView, @selector(webViewCanCheckGeolocationAuthorizationStatus:)))
182        return;
183#endif
184
185    if (!_isSuspended) {
186        dispatch_async(dispatch_get_main_queue(), ^{
187            if (!_coreLocationProvider) {
188                ASSERT(!_coreLocationUpdateListenerProxy);
189                _coreLocationUpdateListenerProxy = adoptNS([[_WebCoreLocationUpdateThreadingProxy alloc] initWithProvider:self]);
190                _coreLocationProvider = adoptNS([[WebGeolocationCoreLocationProvider alloc] initWithListener:_coreLocationUpdateListenerProxy.get()]);
191            }
192            [_coreLocationProvider start];
193        });
194    }
195
196    // We send the lastPosition asynchronously because WebKit does not handle updating the position synchronously.
197    // On WebKit2, we could skip that and send the position directly from the UIProcess.
198    _pendingInitialPositionWebView.add(webView);
199    if (!_sendLastPositionAsynchronouslyTimer)
200        _sendLastPositionAsynchronouslyTimer = [NSTimer scheduledTimerWithTimeInterval:0
201                                                                                target:self
202                                                                              selector:@selector(_handlePendingInitialPosition:)
203                                                                              userInfo:nil
204                                                                               repeats:NO];
205}
206
207- (void)unregisterWebView:(WebView *)webView
208{
209    ASSERT(WebThreadIsLockedOrDisabled());
210
211    if (!_registeredWebViews.contains(webView))
212        return;
213
214    _registeredWebViews.remove(webView);
215    _pendingInitialPositionWebView.remove(webView);
216
217    if (_registeredWebViews.isEmpty()) {
218        dispatch_async(dispatch_get_main_queue(), ^{
219            [_coreLocationProvider stop];
220        });
221        _enableHighAccuracy = NO;
222        _lastPosition.clear();
223    }
224}
225
226- (WebGeolocationPosition *)lastPosition
227{
228    ASSERT(WebThreadIsLockedOrDisabled());
229    return _lastPosition.get();
230}
231
232- (void)setEnableHighAccuracy:(BOOL)enableHighAccuracy
233{
234    ASSERT(WebThreadIsLockedOrDisabled());
235    _enableHighAccuracy = _enableHighAccuracy || enableHighAccuracy;
236    dispatch_async(dispatch_get_main_queue(), ^{
237        [_coreLocationProvider setEnableHighAccuracy:_enableHighAccuracy];
238    });
239}
240
241- (void)initializeGeolocationForWebView:(WebView *)webView listener:(id<WebGeolocationProviderInitializationListener>)listener
242{
243    ASSERT(WebThreadIsLockedOrDisabled());
244
245#if PLATFORM(IOS)
246    if (!CallUIDelegateReturningBoolean(YES, webView, @selector(webViewCanCheckGeolocationAuthorizationStatus:)))
247        return;
248#endif
249    _webViewsWaitingForCoreLocationAuthorization.add(webView, listener);
250    _trackedWebViews.add(webView);
251
252    dispatch_async(dispatch_get_main_queue(), ^{
253        if (!_coreLocationProvider) {
254            ASSERT(!_coreLocationUpdateListenerProxy);
255            _coreLocationUpdateListenerProxy = adoptNS([[_WebCoreLocationUpdateThreadingProxy alloc] initWithProvider:self]);
256            _coreLocationProvider = adoptNS([[WebGeolocationCoreLocationProvider alloc] initWithListener:_coreLocationUpdateListenerProxy.get()]);
257        }
258        [_coreLocationProvider requestGeolocationAuthorization];
259    });
260}
261
262- (void)geolocationAuthorizationGranted
263{
264    ASSERT(WebThreadIsCurrent());
265
266    GeolocationInitializationCallbackMap requests;
267    requests.swap(_webViewsWaitingForCoreLocationAuthorization);
268
269    for (const auto& slot : requests)
270        [slot.value initializationAllowedWebView:slot.key.get()];
271}
272
273- (void)geolocationAuthorizationDenied
274{
275    ASSERT(WebThreadIsCurrent());
276
277    GeolocationInitializationCallbackMap requests;
278    requests.swap(_webViewsWaitingForCoreLocationAuthorization);
279
280    for (const auto& slot : requests)
281        [slot.value initializationDeniedWebView:slot.key.get()];
282}
283
284- (void)stopTrackingWebView:(WebView*)webView
285{
286    ASSERT(WebThreadIsLockedOrDisabled());
287    _trackedWebViews.remove(webView);
288}
289
290#pragma mark - Mirror to WebGeolocationCoreLocationUpdateListener called by the proxy.
291
292- (void)positionChanged:(WebGeolocationPosition*)position
293{
294    ASSERT(WebThreadIsCurrent());
295
296    abortSendLastPosition(self);
297
298    _lastPosition = position;
299    Vector<WebView*> webViewsCopy;
300    copyToVector(_registeredWebViews, webViewsCopy);
301    for (size_t i = 0; i < webViewsCopy.size(); ++i)
302        [webViewsCopy.at(i) _geolocationDidChangePosition:_lastPosition.get()];
303}
304
305- (void)errorOccurred:(NSString *)errorMessage
306{
307    ASSERT(WebThreadIsCurrent());
308
309    _lastPosition.clear();
310
311    Vector<WebView*> webViewsCopy;
312    copyToVector(_registeredWebViews, webViewsCopy);
313    for (size_t i = 0; i < webViewsCopy.size(); ++i)
314        [webViewsCopy.at(i) _geolocationDidFailWithMessage:errorMessage];
315}
316
317- (void)resetGeolocation
318{
319    ASSERT(WebThreadIsCurrent());
320
321    if (_isSuspended) {
322        _shouldResetOnResume = YES;
323        return;
324    }
325    // 1) Stop all ongoing Geolocation initialization and tracking.
326    _webViewsWaitingForCoreLocationAuthorization.clear();
327    _registeredWebViews.clear();
328    abortSendLastPosition(self);
329
330    // 2) Reset the views, each frame will register back if needed.
331    Vector<WebView*> webViewsCopy;
332    copyToVector(_trackedWebViews, webViewsCopy);
333    for (size_t i = 0; i < webViewsCopy.size(); ++i)
334        [webViewsCopy.at(i) _resetAllGeolocationPermission];
335}
336@end
337
338#pragma mark - _WebCoreLocationUpdateThreadingProxy implementation.
339@implementation _WebCoreLocationUpdateThreadingProxy {
340    WebGeolocationProviderIOS* _provider;
341}
342
343- (id)initWithProvider:(WebGeolocationProviderIOS*)provider
344{
345    self = [super init];
346    if (self)
347        _provider = provider;
348    return self;
349}
350
351- (void)geolocationAuthorizationGranted
352{
353    WebThreadRun(^{
354        [_provider geolocationAuthorizationGranted];
355    });
356}
357
358- (void)geolocationAuthorizationDenied
359{
360    WebThreadRun(^{
361        [_provider geolocationAuthorizationDenied];
362    });
363}
364
365- (void)positionChanged:(WebCore::GeolocationPosition*)position
366{
367    RetainPtr<WebGeolocationPosition> webPosition = adoptNS([[WebGeolocationPosition alloc] initWithGeolocationPosition:position]);
368    WebThreadRun(^{
369        [_provider positionChanged:webPosition.get()];
370    });
371}
372
373- (void)errorOccurred:(NSString *)errorMessage
374{
375    WebThreadRun(^{
376        [_provider errorOccurred:errorMessage];
377    });
378}
379
380- (void)resetGeolocation
381{
382    WebThreadRun(^{
383        [_provider resetGeolocation];
384    });
385}
386@end
387
388#endif // PLATFORM(IOS)
389