blob: ba3516dd0930da4f42762cdca210af38772784d2 [file] [log] [blame]
/*
* Copyright (C) 2008, 2009 Apple Inc. All Rights Reserved.
* Copyright (C) 2009 Torch Mobile, Inc.
*
* 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. ``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
* 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.
*/
#include "config.h"
#include "Geolocation.h"
#include "Chrome.h"
#include "CurrentTime.h"
#include "Document.h"
#include "DOMWindow.h"
#include "EventNames.h"
#include "Frame.h"
#include "Page.h"
#include "SQLiteDatabase.h"
#include "SQLiteStatement.h"
#include "SQLiteTransaction.h"
#include "SQLValue.h"
namespace WebCore {
static const char* permissionDeniedErrorMessage = "User denied Geolocation";
Geolocation::GeoNotifier::GeoNotifier(Geolocation* geolocation, PassRefPtr<PositionCallback> successCallback, PassRefPtr<PositionErrorCallback> errorCallback, PassRefPtr<PositionOptions> options)
: m_geolocation(geolocation)
, m_successCallback(successCallback)
, m_errorCallback(errorCallback)
, m_options(options)
, m_timer(this, &Geolocation::GeoNotifier::timerFired)
, m_fatalError(0)
{
ASSERT(m_geolocation);
ASSERT(m_successCallback);
// If no options were supplied from JS, we should have created a default set
// of options in JSGeolocationCustom.cpp.
ASSERT(m_options);
}
void Geolocation::GeoNotifier::setFatalError(PassRefPtr<PositionError> error)
{
m_fatalError = error;
m_timer.startOneShot(0);
}
void Geolocation::GeoNotifier::setCachedPosition(Geoposition* cachedPosition)
{
// We do not take owenership from the caller, but add our own ref count.
m_cachedPosition = cachedPosition;
m_timer.startOneShot(0);
}
void Geolocation::GeoNotifier::startTimerIfNeeded()
{
if (m_options->hasTimeout())
m_timer.startOneShot(m_options->timeout() / 1000.0);
}
void Geolocation::GeoNotifier::timerFired(Timer<GeoNotifier>*)
{
m_timer.stop();
if (m_fatalError) {
if (m_errorCallback)
m_errorCallback->handleEvent(m_fatalError.get());
// This will cause this notifier to be deleted.
m_geolocation->fatalErrorOccurred(this);
return;
}
if (m_cachedPosition) {
m_successCallback->handleEvent(m_cachedPosition.get());
m_geolocation->requestReturnedCachedPosition(this);
return;
}
if (m_errorCallback) {
RefPtr<PositionError> error = PositionError::create(PositionError::TIMEOUT, "Timed out");
m_errorCallback->handleEvent(error.get());
}
m_geolocation->requestTimedOut(this);
}
static const char* databaseName = "/CachedPosition.db";
class CachedPositionManager {
public:
CachedPositionManager()
{
if (s_instances++ == 0) {
s_cachedPosition = new RefPtr<Geoposition>;
*s_cachedPosition = readFromDB();
}
}
~CachedPositionManager()
{
if (--s_instances == 0) {
if (*s_cachedPosition)
writeToDB(s_cachedPosition->get());
delete s_cachedPosition;
}
}
void setCachedPosition(Geoposition* cachedPosition)
{
// We do not take owenership from the caller, but add our own ref count.
*s_cachedPosition = cachedPosition;
}
Geoposition* cachedPosition()
{
return s_cachedPosition->get();
}
static void setDatabasePath(String databasePath)
{
s_databaseFile = databasePath + databaseName;
// If we don't have have a cached position, attempt to read one from the
// DB at the new path.
if (s_instances && *s_cachedPosition == 0)
*s_cachedPosition = readFromDB();
}
private:
static PassRefPtr<Geoposition> readFromDB()
{
SQLiteDatabase database;
if (!database.open(s_databaseFile))
return 0;
// Create the table here, such that even if we've just created the
// DB, the commands below should succeed.
if (!database.executeCommand("CREATE TABLE IF NOT EXISTS CachedPosition ("
"latitude REAL NOT NULL, "
"longitude REAL NOT NULL, "
"altitude REAL, "
"accuracy REAL NOT NULL, "
"altitudeAccuracy REAL, "
"heading REAL, "
"speed REAL, "
"timestamp INTEGER NOT NULL)"))
return 0;
SQLiteStatement statement(database, "SELECT * FROM CachedPosition");
if (statement.prepare() != SQLResultOk)
return 0;
if (statement.step() != SQLResultRow)
return 0;
bool providesAltitude = statement.getColumnValue(2).type() != SQLValue::NullValue;
bool providesAltitudeAccuracy = statement.getColumnValue(4).type() != SQLValue::NullValue;
bool providesHeading = statement.getColumnValue(5).type() != SQLValue::NullValue;
bool providesSpeed = statement.getColumnValue(6).type() != SQLValue::NullValue;
RefPtr<Coordinates> coordinates = Coordinates::create(statement.getColumnDouble(0), // latitude
statement.getColumnDouble(1), // longitude
providesAltitude, statement.getColumnDouble(2), // altitude
statement.getColumnDouble(3), // accuracy
providesAltitudeAccuracy, statement.getColumnDouble(4), // altitudeAccuracy
providesHeading, statement.getColumnDouble(5), // heading
providesSpeed, statement.getColumnDouble(6)); // speed
return Geoposition::create(coordinates.release(), statement.getColumnInt64(7)); // timestamp
}
static void writeToDB(Geoposition* position)
{
ASSERT(position);
SQLiteDatabase database;
if (!database.open(s_databaseFile))
return;
SQLiteTransaction transaction(database);
if (!database.executeCommand("DELETE FROM CachedPosition"))
return;
SQLiteStatement statement(database, "INSERT INTO CachedPosition ("
"latitude, "
"longitude, "
"altitude, "
"accuracy, "
"altitudeAccuracy, "
"heading, "
"speed, "
"timestamp) "
"VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
if (statement.prepare() != SQLResultOk)
return;
statement.bindDouble(1, position->coords()->latitude());
statement.bindDouble(2, position->coords()->longitude());
if (position->coords()->canProvideAltitude())
statement.bindDouble(3, position->coords()->altitude());
else
statement.bindNull(3);
statement.bindDouble(4, position->coords()->accuracy());
if (position->coords()->canProvideAltitudeAccuracy())
statement.bindDouble(5, position->coords()->altitudeAccuracy());
else
statement.bindNull(5);
if (position->coords()->canProvideHeading())
statement.bindDouble(6, position->coords()->heading());
else
statement.bindNull(6);
if (position->coords()->canProvideSpeed())
statement.bindDouble(7, position->coords()->speed());
else
statement.bindNull(7);
statement.bindInt64(8, position->timestamp());
if (!statement.executeCommand())
return;
transaction.commit();
}
static int s_instances;
static RefPtr<Geoposition>* s_cachedPosition;
static String s_databaseFile;
};
int CachedPositionManager::s_instances = 0;
RefPtr<Geoposition>* CachedPositionManager::s_cachedPosition;
String CachedPositionManager::s_databaseFile;
Geolocation::Geolocation(Frame* frame)
: EventListener(GeolocationEventListenerType)
, m_frame(frame)
, m_service(GeolocationService::create(this))
, m_allowGeolocation(Unknown)
, m_shouldClearCache(false)
, m_cachedPositionManager(new CachedPositionManager)
{
if (!m_frame)
return;
ASSERT(m_frame->document());
m_frame->document()->setUsingGeolocation(true);
if (m_frame->domWindow())
m_frame->domWindow()->addEventListener(eventNames().unloadEvent, this, false);
}
Geolocation::~Geolocation()
{
if (m_frame && m_frame->domWindow())
m_frame->domWindow()->removeEventListener(eventNames().unloadEvent, this, false);
}
void Geolocation::disconnectFrame()
{
m_service->stopUpdating();
if (m_frame && m_frame->document())
m_frame->document()->setUsingGeolocation(false);
m_frame = 0;
delete m_cachedPositionManager;
}
void Geolocation::getCurrentPosition(PassRefPtr<PositionCallback> successCallback, PassRefPtr<PositionErrorCallback> errorCallback, PassRefPtr<PositionOptions> options)
{
RefPtr<GeoNotifier> notifier = makeRequest(successCallback, errorCallback, options);
ASSERT(notifier);
m_oneShots.add(notifier);
}
int Geolocation::watchPosition(PassRefPtr<PositionCallback> successCallback, PassRefPtr<PositionErrorCallback> errorCallback, PassRefPtr<PositionOptions> options)
{
RefPtr<GeoNotifier> notifier = makeRequest(successCallback, errorCallback, options);
ASSERT(notifier);
static int sIdentifier = 0;
m_watchers.set(++sIdentifier, notifier);
return sIdentifier;
}
PassRefPtr<Geolocation::GeoNotifier> Geolocation::makeRequest(PassRefPtr<PositionCallback> successCallback, PassRefPtr<PositionErrorCallback> errorCallback, PassRefPtr<PositionOptions> options)
{
RefPtr<GeoNotifier> notifier = GeoNotifier::create(this, successCallback, errorCallback, options);
// Check whether permissions have already been denied. Note that if this is the case,
// the permission state can not change again in the lifetime of this page.
if (isDenied()) {
notifier->setFatalError(PositionError::create(PositionError::PERMISSION_DENIED, permissionDeniedErrorMessage));
} else {
if (haveSuitableCachedPosition(notifier->m_options.get())) {
ASSERT(m_cachedPositionManager->cachedPosition());
if (isAllowed())
notifier->setCachedPosition(m_cachedPositionManager->cachedPosition());
else {
m_requestsAwaitingCachedPosition.add(notifier);
requestPermission();
}
} else {
if (m_service->startUpdating(notifier->m_options.get()))
notifier->startTimerIfNeeded();
else
notifier->setFatalError(PositionError::create(PositionError::UNKNOWN_ERROR, "Failed to start Geolocation service"));
}
}
return notifier.release();
}
void Geolocation::fatalErrorOccurred(Geolocation::GeoNotifier* notifier)
{
// This request has failed fatally. Remove it from our lists.
m_oneShots.remove(notifier);
for (GeoNotifierMap::iterator iter = m_watchers.begin(); iter != m_watchers.end(); ++iter) {
if (iter->second == notifier) {
m_watchers.remove(iter);
break;
}
}
if (!hasListeners())
m_service->stopUpdating();
}
void Geolocation::requestTimedOut(GeoNotifier* notifier)
{
// If this is a one-shot request, stop it.
m_oneShots.remove(notifier);
if (!hasListeners())
m_service->stopUpdating();
}
void Geolocation::requestReturnedCachedPosition(GeoNotifier* notifier)
{
// If this is a one-shot request, stop it.
if (m_oneShots.contains(notifier)) {
m_oneShots.remove(notifier);
if (!hasListeners())
m_service->stopUpdating();
return;
}
// Otherwise, start the service to get updates.
if (m_service->startUpdating(notifier->m_options.get()))
notifier->startTimerIfNeeded();
else
notifier->setFatalError(PositionError::create(PositionError::UNKNOWN_ERROR, "Failed to start Geolocation service"));
}
bool Geolocation::haveSuitableCachedPosition(PositionOptions* options)
{
if (m_cachedPositionManager->cachedPosition() == 0)
return false;
if (!options->hasMaximumAge())
return true;
if (options->maximumAge() == 0)
return false;
DOMTimeStamp currentTimeMillis = currentTime() * 1000.0;
return m_cachedPositionManager->cachedPosition()->timestamp() > currentTimeMillis - options->maximumAge();
}
void Geolocation::clearWatch(int watchId)
{
m_watchers.remove(watchId);
if (!hasListeners())
m_service->stopUpdating();
}
void Geolocation::suspend()
{
if (hasListeners())
m_service->suspend();
}
void Geolocation::resume()
{
if (hasListeners())
m_service->resume();
}
void Geolocation::setIsAllowed(bool allowed)
{
// This may be due to either a new position from the service, or a cached
// position.
m_allowGeolocation = allowed ? Yes : No;
if (!isAllowed()) {
RefPtr<WebCore::PositionError> error = PositionError::create(PositionError::PERMISSION_DENIED, permissionDeniedErrorMessage);
error->setIsFatal(true);
handleError(error.get());
return;
}
// If the service has a last position, use it to call back for all requests.
// If any of the requests are waiting for permission for a cached position,
// the position from the service will be at least as fresh.
if (m_service->lastPosition())
makeSuccessCallbacks();
else {
GeoNotifierSet::const_iterator end = m_requestsAwaitingCachedPosition.end();
for (GeoNotifierSet::const_iterator iter = m_requestsAwaitingCachedPosition.begin(); iter != end; ++iter)
(*iter)->setCachedPosition(m_cachedPositionManager->cachedPosition());
}
m_requestsAwaitingCachedPosition.clear();
}
void Geolocation::sendError(Vector<RefPtr<GeoNotifier> >& notifiers, PositionError* error)
{
Vector<RefPtr<GeoNotifier> >::const_iterator end = notifiers.end();
for (Vector<RefPtr<GeoNotifier> >::const_iterator it = notifiers.begin(); it != end; ++it) {
RefPtr<GeoNotifier> notifier = *it;
if (notifier->m_errorCallback)
notifier->m_errorCallback->handleEvent(error);
}
}
void Geolocation::sendPosition(Vector<RefPtr<GeoNotifier> >& notifiers, Geoposition* position)
{
Vector<RefPtr<GeoNotifier> >::const_iterator end = notifiers.end();
for (Vector<RefPtr<GeoNotifier> >::const_iterator it = notifiers.begin(); it != end; ++it) {
RefPtr<GeoNotifier> notifier = *it;
ASSERT(notifier->m_successCallback);
notifier->m_timer.stop();
notifier->m_successCallback->handleEvent(position);
}
}
void Geolocation::stopTimer(Vector<RefPtr<GeoNotifier> >& notifiers)
{
Vector<RefPtr<GeoNotifier> >::const_iterator end = notifiers.end();
for (Vector<RefPtr<GeoNotifier> >::const_iterator it = notifiers.begin(); it != end; ++it) {
RefPtr<GeoNotifier> notifier = *it;
notifier->m_timer.stop();
}
}
void Geolocation::stopTimersForOneShots()
{
Vector<RefPtr<GeoNotifier> > copy;
copyToVector(m_oneShots, copy);
stopTimer(copy);
}
void Geolocation::stopTimersForWatchers()
{
Vector<RefPtr<GeoNotifier> > copy;
copyValuesToVector(m_watchers, copy);
stopTimer(copy);
}
void Geolocation::stopTimers()
{
stopTimersForOneShots();
stopTimersForWatchers();
}
void Geolocation::handleError(PositionError* error)
{
ASSERT(error);
Vector<RefPtr<GeoNotifier> > oneShotsCopy;
copyToVector(m_oneShots, oneShotsCopy);
Vector<RefPtr<GeoNotifier> > watchersCopy;
copyValuesToVector(m_watchers, watchersCopy);
// Clear the lists before we make the callbacks, to avoid clearing notifiers
// added by calls to Geolocation methods from the callbacks.
m_oneShots.clear();
if (error->isFatal())
m_watchers.clear();
sendError(oneShotsCopy, error);
sendError(watchersCopy, error);
if (!hasListeners())
m_service->stopUpdating();
}
void Geolocation::requestPermission()
{
if (m_allowGeolocation > Unknown)
return;
if (!m_frame)
return;
Page* page = m_frame->page();
if (!page)
return;
m_allowGeolocation = InProgress;
// Ask the chrome: it maintains the geolocation challenge policy itself.
page->chrome()->requestGeolocationPermissionForFrame(m_frame, this);
}
void Geolocation::geolocationServicePositionChanged(GeolocationService*)
{
ASSERT(m_service->lastPosition());
m_cachedPositionManager->setCachedPosition(m_service->lastPosition());
// Stop all currently running timers.
stopTimers();
if (!isAllowed()) {
// requestPermission() will ask the chrome for permission. This may be
// implemented synchronously or asynchronously. In both cases,
// makeSucessCallbacks() will be called if permission is granted, so
// there's nothing more to do here.
requestPermission();
return;
}
makeSuccessCallbacks();
}
void Geolocation::makeSuccessCallbacks()
{
ASSERT(m_service->lastPosition());
ASSERT(isAllowed());
Vector<RefPtr<GeoNotifier> > oneShotsCopy;
copyToVector(m_oneShots, oneShotsCopy);
Vector<RefPtr<GeoNotifier> > watchersCopy;
copyValuesToVector(m_watchers, watchersCopy);
// Clear the lists before we make the callbacks, to avoid clearing notifiers
// added by calls to Geolocation methods from the callbacks.
m_oneShots.clear();
sendPosition(oneShotsCopy, m_service->lastPosition());
sendPosition(watchersCopy, m_service->lastPosition());
if (!hasListeners())
m_service->stopUpdating();
}
void Geolocation::geolocationServiceErrorOccurred(GeolocationService* service)
{
ASSERT(service->lastError());
handleError(service->lastError());
}
bool Geolocation::operator==(const EventListener& listener)
{
if (listener.type() != GeolocationEventListenerType)
return false;
const Geolocation* geolocation = static_cast<const Geolocation*>(&listener);
return m_frame == geolocation->m_frame;
}
void Geolocation::handleEvent(ScriptExecutionContext*, Event* event)
{
ASSERT_UNUSED(event, event->type() == eventNames().unloadEvent);
// Cancel any ongoing requests on page unload. This is required to release
// references to JS callbacks in the page, to allow the frame to be cleaned up
// by WebKit.
m_oneShots.clear();
m_watchers.clear();
}
void Geolocation::setDatabasePath(String databasePath)
{
CachedPositionManager::setDatabasePath(databasePath);
}
} // namespace WebCore