From 2e852c116de864f4dea2d2f08317839c3bf97163 Mon Sep 17 00:00:00 2001
From: Apertis CI <devel@lists.apertis.org>
Date: Fri, 7 Mar 2025 17:51:57 +0000
Subject: [PATCH] Import Upstream version 2.7.2

---
 .editorconfig                       |  24 +
 .gitlab-ci.yml                      |  28 +-
 HACKING.md                          |   4 +-
 NEWS                                |  88 +++-
 README.md                           |  64 +--
 data/geoclue.5.in                   | 115 ++++-
 data/geoclue.conf.in                |  47 +-
 data/geoclue.service.in             |   1 +
 data/meson.build                    |  14 +-
 demo/agent.c                        |  78 +--
 demo/gclue-service-agent.c          |  20 +-
 demo/where-am-i.c                   |   6 +-
 interface/fi.w1.wpa_supplicant1.xml |  74 ++-
 libgeoclue/gclue-helpers.c          |   3 +-
 libgeoclue/gclue-simple.c           | 158 +++---
 meson.build                         |  13 +-
 meson_options.txt                   |   9 +-
 src/gclue-3g-tower.h                |   7 +-
 src/gclue-3g.c                      | 214 +++++++--
 src/gclue-3g.h                      |  10 +-
 src/gclue-cdma.c                    |  17 +-
 src/gclue-client-info.c             |  13 +-
 src/gclue-compass.c                 |  21 +-
 src/gclue-config.c                  | 520 ++++++++++++++------
 src/gclue-config.h                  |   2 +
 src/gclue-location-source.c         |  55 ++-
 src/gclue-location-source.h         |   4 +
 src/gclue-location.c                | 224 +++++----
 src/gclue-location.h                |  26 +-
 src/gclue-locator.c                 | 113 ++++-
 src/gclue-main.c                    |   7 +-
 src/gclue-modem-gps.c               |  41 +-
 src/gclue-modem-manager.c           | 580 +++++++++++++++-------
 src/gclue-mozilla.c                 | 412 ++++++++++++----
 src/gclue-mozilla.h                 |  61 ++-
 src/gclue-nmea-source.c             | 716 ++++++++++++++++++++--------
 src/gclue-nmea-utils.c              |  36 ++
 src/gclue-nmea-utils.h              |   1 +
 src/gclue-service-client.c          |  27 +-
 src/gclue-service-manager.c         |  89 ++--
 src/gclue-static-source.c           | 491 +++++++++++++++++++
 src/gclue-static-source.h           |  40 ++
 src/gclue-utils.h                   |  61 +++
 src/gclue-web-source.c              | 390 +++++++++++----
 src/gclue-web-source.h              |   8 +-
 src/gclue-wifi.c                    | 619 ++++++++++++++++--------
 src/gclue-wifi.h                    |   3 +-
 src/meson.build                     |   8 +-
 48 files changed, 4137 insertions(+), 1425 deletions(-)
 create mode 100644 .editorconfig
 create mode 100644 src/gclue-static-source.c
 create mode 100644 src/gclue-static-source.h
 create mode 100644 src/gclue-utils.h

diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..56ff6a0
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,24 @@
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+trim_trailing_whitespace = true
+
+[*.[ch]]
+indent_size = 8
+indent_style = space
+insert_final_newline = true
+# For the legacy tabs which still exist in the code:
+tab_width = 8
+
+[*.xml]
+indent_size = 2
+indent_style = space
+
+[meson.build]
+indent_size = 4
+indent_style = space
+
+[*.md]
+max_line_length = 80
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index bea3b69..ba4baa6 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,28 +1,26 @@
 before_script:
-    -  sed -i '/^#\sdeb-src /s/^#//' '/etc/apt/sources.list'
+    - sed -i 's/^\(Types. .*\)$/\1 deb-src/' /etc/apt/sources.list.d/debian.sources
     - apt-get --allow-unauthenticated update && apt-get build-dep --yes geoclue-2.0
-    - apt-get install --yes git gobject-introspection libmm-glib-dev wget valac
-    - apt-get install --yes libnotify-dev xsltproc gtk-doc-tools python3-pip
-    - apt-get install --yes ninja-build gettext modemmanager-dev
-    - pip3 install meson==0.53.2
 
-# Ubuntu 14.04 is not supported, see README for details
-#
+workflow:
+    rules:
+        - if: $CI_PIPELINE_SOURCE == 'merge_request_event'
+        - if: $CI_PIPELINE_SOURCE == 'push'
 
-ubuntu-18.04:
-    image: ubuntu:bionic
+debian-bookworm:
+    image: debian:bookworm
     artifacts:
         when: always
-        name: "bionic-${CI_COMMIT_REF_NAME}"
+        name: "bookworm-${CI_COMMIT_REF_NAME}"
         paths:
             - "${CI_PROJECT_DIR}/build"
-    script: meson build && ninja -C build && ninja -C build test && ninja -C build install
+    script: meson setup build && ninja -C build && ninja -C build test && ninja -C build install
 
-ubuntu-18.04-no-backend:
-    image: ubuntu:bionic
+debian-bookworm-no-backend:
+    image: debian:bookworm
     artifacts:
         when: always
-        name: "bionic-no-backend-${CI_COMMIT_REF_NAME}"
+        name: "bookworm-no-backend-${CI_COMMIT_REF_NAME}"
         paths:
             - "${CI_PROJECT_DIR}/build"
-    script: meson -Denable-backend=false build && ninja -C build && ninja -C build test && ninja -C build install
+    script: meson setup -Denable-backend=false build && ninja -C build && ninja -C build test && ninja -C build install
diff --git a/HACKING.md b/HACKING.md
index ef63ca0..8aea59b 100644
--- a/HACKING.md
+++ b/HACKING.md
@@ -4,10 +4,10 @@
   is already included in your distro/OS, you should be able to use the
   package manager's command to install all build depedndancies.
 
-  * gio (>= 2.44.0)
+  * gio
   * gobject-introspection
   * json-glib
-  * libsoup2.4 (>= 2.42)
+  * libsoup3.0
   * pkg-config
 
   Fedora:
diff --git a/NEWS b/NEWS
index 9a6d731..6dcfebb 100644
--- a/NEWS
+++ b/NEWS
@@ -1,3 +1,81 @@
+2.7.2
+=====
+
+- Don't warn about missing conf.d directory
+- Accept 11-part RMC NMEA sentences from pre NMEA v2.3 GPS sources
+- Log the service client list on SIGUSR1
+- Read GPS locations from non-enabled ModemManager modem, enabling GPS use without a SIM card
+- Bump ModemManager dependency to version 1.12
+- Mozilla Location Service (MLS) has been retired, remove MLS related constants and configs:
+  - Remove DEFAULT_WIFI_URL and DEFAULT_WIFI_SUBMIT_URL
+  - If wifi URL is not set in config, disable wifi and 3g sources
+  - Remove Mozilla URLs from default config file
+  - Remove mozilla-api-key option from meson build
+- Allow specifying default Ichnaea-compatible locate and submit URLs in meson build
+- Allow setting an empty submission-nick in config
+- Add User-Agent header to locate and submit queries
+- Upgrade GLib / Gio dependency to version 2.74.0
+- Use GApplication in demo agent, ensuring a unique, user session bound process
+- The async constructor gclue_simple_new in libgeoclue now can be canceled properly
+
+Contributors:
+- Kira Bruneau
+- Teemu Ikonen
+- Maciej S. Szmigiero
+- Jan Alexander Steffens (heftig)
+- Balló György
+- Markus Göllnitz
+
+2.7.1
+=====
+
+- Add 'age' field to MLS locate queries
+- Location updates now always have an accuracy value
+- Improvements to NMEA parsing:
+  - Parse NMEA timestamps with sub-second accuracy
+  - Add default accuracy to NMEA RMC locations
+  - Ignore locations from GGA and RMC sentences if the GNSS fix is not valid
+- Prioritize GNSS sources with a recent fix over other sources, preventing location jumps
+- Install D-Bus policy in /usr/share, not /etc
+- Upgrade GLib / Gio dependency to version 2.68.0
+- Correct gi annotations in GClueSimple
+- Various small fixes
+
+Contributors:
+- Chris Talbot
+- Gioele Barabucci
+- Philipp Hörist
+- Teemu Ikonen
+
+2.7.0
+=====
+
+- Multiple config files named *.conf are now read from the config directory at @sysconfdir@/geoclue/conf.d,
+- HTTP requests are now made via libsoup3.0 instead of libsoup2.4,
+- A static location can now be set in @sysconfdir@/geolocation for immobile systems,
+- Web source requests are now submitted with combined WiFi and 3GPP tower data,
+- Web source now checks connectivity in a way that allows location and submission servers running on localhost,
+- Web source submissions are now made using /v2/geosubmit API,
+- Web source cell tower submissions now have the correct radio type,
+- Web source requests now submit the BSS age property,
+- Web source submissions now contain the location speed,
+- Web source cache now respects WiFi signal tolerance strictly,
+- NMEA source now supports both '\n' and '\r' NMEA delimiters,
+- NMEA source can now be made the Web source submit source,
+- ModemManager now use signaled calls to get cached location information to avoid performing explicit modem query,
+- Location description now contains information about its source,
+- GSettings backend no longer complains about being run from a read-only filesystem,
+- Many small improvements and fixes, some memory safety related.
+
+Contributors:
+- Andrey Skvortsov
+- Carlos Garcia Campos
+- Dylan Van Assche
+- Shoji Keita
+- Teemu Ikonen
+- Maciej S. Szmigiero
+- Maximiliano Sandoval R
+
 2.6.0
 =====
 
@@ -12,9 +90,9 @@
 - Modem manager: add polkit rule to allow GPS access
 - Allow disabling compass at build and at runtime
 - Fix heading computation for identical locations
-- Be strict with time and distance threshold 
+- Be strict with time and distance threshold
 - Fix the XDG location portal integration
-- Replace agent wait timeout with a queue 
+- Replace agent wait timeout with a queue
 - Other bugs fixes
 
 Contributors:
@@ -22,10 +100,10 @@ Contributors:
 - Laurent Bigonville
 - Angus Ainslie
 - Dan Nicholson
-- Guido Günther 
+- Guido Günther
 - Jan Alexander Steffens
-- Abderrahim Kitouni 
-- clayton craft 
+- Abderrahim Kitouni
+- clayton craft
 - Ian Douglas Scott
 - Chupligin Sergey
 - Dor Askayo
diff --git a/README.md b/README.md
index 6242120..b3f7d90 100644
--- a/README.md
+++ b/README.md
@@ -6,67 +6,73 @@ is to make creating location-aware applications as simple as possible.
 
 Geoclue is Free Software, licensed under GNU GPLv2+.
 
-Geoclue comprises the following functionalities : 
+Geoclue comprises the following functionalities :
 - WiFi-based geolocation (accuracy: in meters)
 - GPS(A) receivers (accuracy: in centimeters)
-- GPS of other devices on the local network, e.g smartphones (accuracy: 
+- GPS of other devices on the local network, e.g smartphones (accuracy:
   in centimeters)
 - 3G modems (accuracy: in kilometers, unless modem has GPS)
-- GeoIP (accuracy: city-level)
-
-WiFi-based geolocation makes use of 
-[Mozilla Location Service](https://wiki.mozilla.org/CloudServices/Location). 
-
-If geoclue is unable to find you, you can easily fix that by installing 
-and running a 
-[simple app](https://wiki.mozilla.org/CloudServices/Location#Contributing) on 
-your phone. For using phone GPS, you'll need to install the latest version of 
+- Static location source (reads location from a system-wide file)
+
+WiFi and cell tower-based geolocation used to use
+[Mozilla Location Service](https://wiki.mozilla.org/CloudServices/Location),
+which closed on June 12th, 2024. Geoclue currently does not enable WiFi location
+by default. Some potential replacements for MLS are:
+- [Positon](https://positon.xyz/). *Free-of-charge, network-based location
+service for the open-source software community.* Uses data from a commercial
+location service, requires API keys.
+- [BeaconDB](https://beacondb.net/). *Public domain wireless geolocation
+database.* Crowd-sourced WiFi, Bluetooth and cell tower database.
+- [OLS](https://codeberg.org/tpikonen/ols). *Offline Location Service.*
+Local-data on-device server, mostly useful for devices with GPS.
+
+For using phone GPS, you can install the latest version of
 [GeoclueShare app](https://github.com/ankitstarski/GeoclueShare/releases)
 on your phone (currently, this is supported only on Android devices).
 
-Geoclue was also used for (reverse-)geocoding but that functionality has 
-been dropped in favour of the 
+Geoclue was also used for (reverse-)geocoding but that functionality has
+been dropped in favour of the
 [geocode-glib library](http://ftp.gnome.org/pub/GNOME/sources/geocode-glib/).
 
 # History
-Geoclue was started during the GNOME Summit 2006 in Boston. At least 
-Keith Preston and Tuomas Kuosmanen can be blamed. There was a total rewrite 
+Geoclue was started during the GNOME Summit 2006 in Boston. At least
+Keith Preston and Tuomas Kuosmanen can be blamed. There was a total rewrite
 after version 0.9.
 
-There was yet another rewrite that we call geoclue2. The first version to 
+There was yet another rewrite that we call geoclue2. The first version to
 introduce the re-write was version 1.99.
 
 # Communication and Contribution
 
-- Discussions take place on the 
+- Discussions take place on the
 [GNOME Discourse](https://discourse.gnome.org/c/platform).
 - The IRC chat for geoclue is on __#gnome-maps__ at irc.gimp.org .
-- Issues are tracked on 
+- Issues are tracked on
 [Gitlab](https://gitlab.freedesktop.org/geoclue/geoclue/issues).
 
 # Get Source Code
 The source code is available as a tar-ball and in a Git repository.
 
-For latest release tarballs, use the `Download` option of Gitlab on the 
+For latest release tarballs, use the `Download` option of Gitlab on the
 [tag of your choice](https://gitlab.freedesktop.org/geoclue/geoclue/tags/).
 
-Older (than 2.4.13) releases are available 
+Older (than 2.4.13) releases are available
 [here](http://www.freedesktop.org/software/geoclue/releases/2.4/).
 
 Git repository for Geoclue: https://gitlab.freedesktop.org/geoclue/geoclue
-  
+
 # Building Geoclue
 
-The guidelines for building geoclue have been documented 
-[here](https://gitlab.freedesktop.org/geoclue/geoclue/blob/master/HACKING.md). 
+The guidelines for building geoclue have been documented
+[here](https://gitlab.freedesktop.org/geoclue/geoclue/blob/master/HACKING.md).
 
 # Using Geoclue in an application
- 
-- __D-Bus API__: The documentation for using geoclue with D-Bus API is 
+
+- __D-Bus API__: The documentation for using geoclue with D-Bus API is
 [here](http://www.freedesktop.org/software/geoclue/docs/).
-- __Libgeoclue API documentation__:  The documentation is available 
+- __Libgeoclue API documentation__:  The documentation is available
 [here](https://www.freedesktop.org/software/geoclue/docs/libgeoclue/).
-- __C user application__: 
+- __C user application__:
 [Here](https://gitlab.freedesktop.org/geoclue/geoclue/blob/master/demo/where-am-i.c)
-is an example showing a C application that uses 
-geoclue to locate its user. 
+is an example showing a C application that uses
+geoclue to locate its user.
diff --git a/data/geoclue.5.in b/data/geoclue.5.in
index b6a5795..1f21995 100644
--- a/data/geoclue.5.in
+++ b/data/geoclue.5.in
@@ -1,43 +1,59 @@
-.TH "GEOCLUE CONFIGURATION" 5 
+.TH "GEOCLUE CONFIGURATION" 5
 .SH NAME
 geoclue.conf
 \-
 geoclue configuration parameters
 .SH SYNOPSIS
-.B @sysconfdir@/geoclue/geoclue.conf
+Main configuration file: @sysconfdir@/geoclue/geoclue.conf
+.br
+Overwriting parameters files: @sysconfdir@/geoclue/conf.d
 .SH DESCRIPTION
 .ad
 .fi
-The geoclue geoclue.conf configuration file specifies parameters that
-control the operation of geoclue.
+The main GeoClue configuration file 'geoclue.conf' specifies parameters
+that control the operation of geoclue. Parameters can be overwritten by
+placing configuration files in conf.d directory and applied in alphabetic
+order. Thus, a configuration file '90-config.conf' will overwrite parameters
+specified in another configuration file '50-config.conf' in the conf.d
+directory.
 .PP
 All configurations settings below are mandatory and the defaults are
 what you see before you edit them in geoclue.conf. If you want to keep the default
 values around, copy and comment out the appropriate line(s) before
 changing them.
+.PP
+Missing 'enable' key for a particular source in the main configuration file
+causes that source to be enabled by default. Adding 'enable' key setting
+to any further config file can overwrite this default.
 .SH AGENT CONFIGURATION OPTIONS
 .B \fI[agent]
 is used to begin the agent configuration.
-.IP \fBwhitelist 
+.IP \fBwhitelist
 .br
 Whitelist of desktop IDs (without .desktop part) of all agents we recognise,
 separated by a ';'.
-.IP 
+.IP
 .B whitelist=geoclue-demo-agent;gnome-shell;io.elementary.desktop.agent-geoclue2
 .br
 .IP \fB[network-nmea]
 .br
 Network NMEA source configuration options
 .IP
-.B \fBenable=true 
+.B \fBenable=true
 .br
 Fetch location from NMEA sources on local network?
 .br
-.IP \fB[3G]
+.IP
+.B \fBnmea-socket=/var/run/gps-share.sock
+.br
+Use a nmea unix socket as the data source.
+If not set, unix socket will not be used.
+.br
+.IP \fB[3g]
 .br
 3G source configuration options
 .IP
-.B \fBenable=true 
+.B \fBenable=true
 .br
 Enable 3G source
 .br
@@ -45,7 +61,7 @@ Enable 3G source
 .br
 CDMA source configuration options
 .IP
-.B \fBenable=true 
+.B \fBenable=true
 .br
 Enable CDMA source
 .br
@@ -53,7 +69,7 @@ Enable CDMA source
 .br
 Modem GPS source configuration options
 .IP
-.B \fBenable=true 
+.B \fBenable=true
 .br
 Enable Modem-GPS source
 .br
@@ -61,14 +77,15 @@ Enable Modem-GPS source
 .br
 WiFi source configuration options
 .IP
-.B \fBenable=true 
+.B \fBenable=true
 .br
 Enable WiFi source
 .IP
-.B url=\fIhttps://location.services.mozilla.com/v1/geolocate?key=geoclue
+.B url=\fIhttps://location.services.mozilla.com/v1/geolocate?key=YOUR_KEY
 .br
-URL to the wifi geolocation service. The key can currenty be anything, just
-needs to be present but that is likely going to change in future.
+URL to the WiFi geolocation service. If not set, defaults to Mozilla's
+Location Service with a hardcoded key. To use a custom key, uncomment this URL
+while changing YOUR_KEY to your MLS API key.
 .IP
 .B submit-data=false
 Submit data to Mozilla Location Service
@@ -76,16 +93,37 @@ Submit data to Mozilla Location Service
 If set to true, geoclue will automatically submit network data to Mozilla
 each time it gets a GPS lock.
 .IP
-.B submission-url=\fIhttps://location.services.mozilla.com/v1/submit?key=geoclue
+.B submission-url=\fIhttps://location.services.mozilla.com/v2/geosubmit?key=YOUR_KEY
 .br
 URL to submission API of Mozilla Location Service
 .IP
 .B submission-nick=geoclue
 .br
-A nickname to submit network data with. A nickname must be 2-32 characters long.
+A nickname to submit network data with. If empty, omitted from the submission. Otherwise, must be 2 to 32 characters long. Defaults to "geoclue".
+.br
+.IP \fB[compass]
+.br
+Compass configuration options
+.IP
+.B \fBenable=true
+.br
+Enable Compass
+.br
+.IP \fB[static-source]
+.br
+Static source configuration options.
+.br
+This source reads location from "geolocation" file in @sysconfdir@. While this file is constantly monitored for changes during geoclue operation, and the reported static location is updated accordingly, this source isn't meant for inputting a dynamically changing location to geoclue (please use the Network NMEA source for that).
+.IP
+.B \fBenable=true
+.br
+Enable the static source.
+.br
+If you make use of this source, you probably should disable other location
+sources in geoclue.conf so they won't override the configured static location.
 .br
 .SH APPLICATION CONFIGURATION OPTIONS
-Having an entry here for an application with 
+Having an entry here for an application with
 .B allowed=true
 means that geoclue will not ask agent to authorize the application. This is to
 ensure that applications with built-in authorization mechanism (e.g web
@@ -165,6 +203,47 @@ system=false
 .br
 users=
 .br
+.SH STATIC LOCATION FILE
+.SS Basic format:
+The static location file in @sysconfdir@ (used by the static source) is a text file consisting of the following:
+.nr step 1 1
+.IP \n[step]
+Latitude (floating point number; positive values mean north, negative south)
+.IP \n+[step]
+Longitude (floating point number; positive values mean east, negative west)
+.IP \n+[step]
+Altitude (floating point number; in meters)
+.IP \n+[step]
+Accuracy radius (floating point number; in meters)
+.RE
+.PP
+These values need to be separated by newline characters.
+.SS Additional format information:
+.IP \[bu]
+The '\[sh]' character starts a comment, which continues until the end of the current line.
+.IP \[bu]
+Leading and trailing white-space on each line is ignored.
+.IP \[bu]
+Empty lines (or containing just white-space or a comment) are ignored.
+.SS Example:
+.EX
+# Example static location file for a machine inside Statue of Liberty torch
+
+40.6893129   # latitude
+-74.0445531  # longitude
+96           # altitude
+1.83         # accuracy radius (the diameter of the torch is 12 feet)
+.EE
+.SS Notes:
+For extra security, the static location file can be made readable just by the geoclue user:
+.EX
+# chown @dbus_srv_user@ @sysconfdir@/geolocation
+# chmod 600 @sysconfdir@/geolocation
+.EE
+.br
+.SH CLIENT LIST
+Sending SIGUSR1 to a running geoclue process prints the current list of clients to the log.
+.br
 .SH AUTHOR
 .na
 .nf
diff --git a/data/geoclue.conf.in b/data/geoclue.conf.in
index 650470d..0eb59cd 100644
--- a/data/geoclue.conf.in
+++ b/data/geoclue.conf.in
@@ -25,7 +25,7 @@ enable=true
 [3g]
 
 # Enable 3G source
-enable=true
+enable=@default_wifi_enable@
 
 # CDMA source configuration options
 [cdma]
@@ -43,14 +43,16 @@ enable=true
 [wifi]
 
 # Enable WiFi source
-enable=true
+# If this source and the static source below are both disabled a GeoIP-only
+# source will be used instead.
+enable=@default_wifi_enable@
 
-# URL to the WiFi geolocation service. If not set, defaults to Mozilla's
-# Location Service with a hardcoded key. To use a custom key, uncomment this URL
-# while changing YOUR_KEY to your MLS API key.
-#url=https://location.services.mozilla.com/v1/geolocate?key=YOUR_KEY
+# URL to a WiFi geolocation service compatible with the Ichnaea API
+# (https://ichnaea.readthedocs.io/en/latest/api/geolocate.html).
+# An API key can be set by using the 'key' URL parameter.
+#url=https://example.com/v1/geolocate?key=YOUR_KEY
 
-# To use the Google geolocation service instead of Mozilla's, uncomment this URL
+# To use the Google geolocation service, uncomment this URL
 # while changing YOUR_KEY to your Google API key.
 #
 # WARNING: Please make sure that you are complying with the Google's ToS and
@@ -60,18 +62,24 @@ enable=true
 #
 #url=https://www.googleapis.com/geolocation/v1/geolocate?key=YOUR_KEY
 
-# Submit data to Mozilla Location Service
-# If set to true, geoclue will automatically submit network data to Mozilla
+# Submit data to WiFi geolocation service
+# If set to true, geoclue will automatically submit network data
 # each time it gets a GPS lock.
 #
+# Currently, only Modem-GPS or Network NMEA sources are supported as providers
+# of a location to submit (one at a time).
+# If Modem-GPS source is enabled above it will be the exclusive provider
+# (regardless whether the system is actually equipped with such modem),
+# otherwise Network NMEA source will be considered.
+#
 submit-data=false
 
-# URL to submission API of Mozilla Location Service. If not set, defaults to
-# Mozilla's API with a hardcoded key. To use a custom key, uncomment this URL
-# while changing YOUR_KEY to your MLS API key.
-#submission-url=https://location.services.mozilla.com/v1/submit?key=YOUR_KEY
+# URL to submit data to a WiFi geolocation service with an Ichnaea compatible API
+# (https://ichnaea.readthedocs.io/en/latest/api/geosubmit2.html).
+#submission-url=https://example.com/v2/geosubmit?key=YOUR_KEY
 
-# A nickname to submit network data with. A nickname must be 2-32 characters long.
+# A nickname to submit network data with. If empty, omitted from the submission.
+# Otherwise, must be 2 to 32 characters long. Defaults to "geoclue".
 submission-nick=geoclue
 
 # Compass configuration options
@@ -80,6 +88,17 @@ submission-nick=geoclue
 # Enable Compass
 enable=true
 
+# Static source configuration options
+#
+# This source reads location from "geolocation" file in @sysconfdir@ - please
+# consult geoclue(5) man page for the format description of this file.
+[static-source]
+
+# Enable the static source
+# If you make use of this source, you probably should disable other location
+# sources in this file so they won't override the configured static location.
+enable=true
+
 # Application configuration options
 #
 # NOTE: Having an entry here for an application with allowed=true means that
diff --git a/data/geoclue.service.in b/data/geoclue.service.in
index 6449d30..ec5507d 100644
--- a/data/geoclue.service.in
+++ b/data/geoclue.service.in
@@ -5,6 +5,7 @@ Description=Location Lookup Service
 Type=dbus
 BusName=org.freedesktop.GeoClue2
 User=@dbus_srv_user@
+Environment="GSETTINGS_BACKEND=memory"
 ExecStart=@libexecdir@/geoclue
 
 # Filesystem lockdown
diff --git a/data/meson.build b/data/meson.build
index a1fc61f..b22ff55 100644
--- a/data/meson.build
+++ b/data/meson.build
@@ -1,5 +1,6 @@
 if get_option('enable-backend')
     conf = configuration_data()
+    conf.set('sysconfdir', sysconfdir)
 
     if get_option('demo-agent')
         conf.set('demo_agent', 'geoclue-demo-agent;')
@@ -7,6 +8,12 @@ if get_option('enable-backend')
         conf.set('demo_agent', '')
     endif
 
+    if get_option('default-wifi-url') != ''
+        conf.set('default_wifi_enable', 'true')
+    else
+        conf.set('default_wifi_enable', 'false')
+    endif
+
     conf_dir = join_paths(sysconfdir, 'geoclue')
     configure_file(output: 'geoclue.conf',
                    input: 'geoclue.conf.in',
@@ -18,6 +25,9 @@ if get_option('enable-backend')
     conf.set('dbus_srv_user', get_option('dbus-srv-user'))
     conf.set('sysconfdir', sysconfdir)
 
+    confd_dir = join_paths(conf_dir, 'conf.d')
+    install_emptydir(confd_dir)
+
     service_dir = join_paths(datadir, 'dbus-1', 'system-services')
     configure_file(output: 'org.freedesktop.GeoClue2.service',
                    input: 'org.freedesktop.GeoClue2.service.in',
@@ -33,7 +43,7 @@ if get_option('enable-backend')
     # DBus Service policy file
     dbus_service_dir = get_option('dbus-sys-dir')
     if dbus_service_dir == ''
-        dbus_service_dir = join_paths(sysconfdir, 'dbus-1', 'system.d')
+        dbus_service_dir = join_paths(datadir, 'dbus-1', 'system.d')
     endif
     configure_file(output: 'org.freedesktop.GeoClue2.conf',
                    input: 'org.freedesktop.GeoClue2.conf.in',
@@ -48,7 +58,7 @@ if get_option('enable-backend')
     if systemd_unit_dir == ''
         dep = dependency('systemd', required: false)
         if dep.found()
-            systemd_unit_dir = dep.get_pkgconfig_variable('systemdsystemunitdir')
+            systemd_unit_dir = dep.get_variable(pkgconfig: 'systemdsystemunitdir')
         endif
     endif
 
diff --git a/demo/agent.c b/demo/agent.c
index 8e2a3ef..62d0e2f 100644
--- a/demo/agent.c
+++ b/demo/agent.c
@@ -29,36 +29,47 @@
 
 #include "gclue-service-agent.h"
 
-/* Commandline options */
-static gboolean version;
-
 static GOptionEntry entries[] =
 {
         { "version",
           0,
           0,
           G_OPTION_ARG_NONE,
-          &version,
+          NULL,
           N_("Display version number"),
           NULL },
-        { NULL }
+        G_OPTION_ENTRY_NULL
 };
 
 GClueServiceAgent *agent = NULL;
 
+static gint
+handle_local_options_cb (GApplication *app,
+                         GVariantDict *options,
+                         gpointer      user_data)
+{
+        gboolean version;
+
+        if (g_variant_dict_lookup (options, "version", "b", &version)) {
+                g_print ("%s\n", PACKAGE_VERSION);
+                return EXIT_SUCCESS;
+        }
+
+        return -1;
+}
+
 static void
 on_get_bus_ready (GObject      *source_object,
                   GAsyncResult *res,
                   gpointer      user_data)
 {
-        GError *error = NULL;
+        g_autoptr(GError) error = NULL;
         GDBusConnection *connection;
 
         connection = g_bus_get_finish (res, &error);
         if (connection == NULL) {
                 g_critical ("Failed to get connection to system bus: %s",
                             error->message);
-                g_error_free (error);
 
                 exit (-2);
         }
@@ -68,12 +79,32 @@ on_get_bus_ready (GObject      *source_object,
 
 #define ABS_PATH ABS_SRCDIR "/agent"
 
+static void
+activate_cb (GApplication *app,
+             gpointer      user_data)
+{
+        g_bus_get (G_BUS_TYPE_SYSTEM,
+                   NULL,
+                   on_get_bus_ready,
+                   NULL);
+
+        g_application_hold (app);
+}
+
+static void
+is_registered_cb (GApplication *app,
+                  GParamSpec   *pspec,
+                  gpointer      user_data)
+{
+        if (g_application_get_is_registered (app) && g_application_get_is_remote (app))
+                g_message ("Another instance of GeoClue DemoAgent is running.");
+}
+
 int
 main (int argc, char **argv)
 {
-        GMainLoop *main_loop;
-        GError *error = NULL;
-        GOptionContext *context;
+        g_autoptr (GApplication) app = NULL;
+        int status = 0;
 
         setlocale (LC_ALL, "");
 
@@ -84,29 +115,20 @@ main (int argc, char **argv)
 
         notify_init (_("GeoClue"));
 
-        context = g_option_context_new ("- Geoclue Agent service");
-        g_option_context_add_main_entries (context, entries, GETTEXT_PACKAGE);
-        if (!g_option_context_parse (context, &argc, &argv, &error)) {
-                g_critical ("option parsing failed: %s\n", error->message);
-                exit (-1);
-        }
+        app = g_application_new ("org.freedesktop.GeoClue2.DemoAgent",
+                                 G_APPLICATION_DEFAULT_FLAGS);
 
-        if (version) {
-                g_print ("%s\n", PACKAGE_VERSION);
-                exit (0);
-        }
+        g_application_add_main_option_entries (app, entries);
+        g_application_set_option_context_parameter_string (app, "- Geoclue Agent service");
 
-        g_bus_get (G_BUS_TYPE_SYSTEM,
-                   NULL,
-                   on_get_bus_ready,
-                   NULL);
+        g_signal_connect (app, "activate", G_CALLBACK (activate_cb), NULL);
+        g_signal_connect (app, "handle-local-options", G_CALLBACK (handle_local_options_cb), NULL);
+        g_signal_connect (app, "notify::is-registered", G_CALLBACK (is_registered_cb), NULL);
 
-        main_loop = g_main_loop_new (NULL, FALSE);
-        g_main_loop_run (main_loop);
+        status = g_application_run (G_APPLICATION (app), argc, argv);
 
         if (agent != NULL)
                 g_object_unref (agent);
-        g_main_loop_unref (main_loop);
 
-        return 0;
+        return status;
 }
diff --git a/demo/gclue-service-agent.c b/demo/gclue-service-agent.c
index ac96795..8758721 100644
--- a/demo/gclue-service-agent.c
+++ b/demo/gclue-service-agent.c
@@ -192,16 +192,15 @@ on_manager_proxy_ready (GObject      *source_object,
                         gpointer      user_data)
 {
         GClueServiceAgent *agent;
-        GError *error = NULL;
-        agent = GCLUE_SERVICE_AGENT (user_data);
+        g_autoptr(GError) error = NULL;
 
+        agent = GCLUE_SERVICE_AGENT (user_data);
         agent->priv->manager_proxy = g_dbus_proxy_new_for_bus_finish (res,
                                                                       &error);
         if (agent->priv->manager_proxy == NULL) {
                 g_critical ("Failed to create proxy to %s: %s",
                             MANAGER_PATH,
                             error->message);
-                g_error_free (error);
                 return;
         }
 
@@ -335,10 +334,10 @@ gclue_service_agent_handle_authorize_app (GClueAgent            *agent,
         NotifyNotification *notification;
         NotificationData *data;
         GError *error = NULL;
-        char *desktop_file;
+        g_autofree char *desktop_file = NULL;
         GDesktopAppInfo *app_info;
-        char *msg;
-        const char *reason;
+        g_autofree char *msg = NULL;
+        g_autofree char *reason = NULL;
 
         desktop_file = g_strjoin (".", desktop_id, "desktop", NULL);
         app_info = g_desktop_app_info_new (desktop_file);
@@ -351,18 +350,17 @@ gclue_service_agent_handle_authorize_app (GClueAgent            *agent,
 
                 return TRUE;
         }
-        g_free (desktop_file);
 
         msg = g_strdup_printf (_("Allow '%s' to access your location information?"),
                                g_app_info_get_display_name (G_APP_INFO (app_info)));
         reason = g_desktop_app_info_get_string (app_info, "X-Geoclue-Reason");
         if (reason != NULL) {
-                char *tmp = msg;
-                msg = g_strdup_printf ("%s\n\n%s", msg, reason);
-                g_free (tmp);
+                char *tmp = g_strdup_printf ("%s\n\n%s", msg, reason);
+
+                g_clear_pointer (&msg, g_free);
+                msg = tmp;
         }
         notification = notify_notification_new (_("Geolocation"), msg, "dialog-question");
-        g_free (msg);
 
         data = g_slice_new0 (NotificationData);
         data->invocation = invocation;
diff --git a/demo/where-am-i.c b/demo/where-am-i.c
index aa873a4..8324058 100644
--- a/demo/where-am-i.c
+++ b/demo/where-am-i.c
@@ -109,9 +109,9 @@ print_location (GClueSimple *simple)
 
         timestamp = gclue_location_get_timestamp (location);
         if (timestamp) {
-                GDateTime *date_time;
+                g_autoptr(GDateTime) date_time = NULL;
                 guint64 sec, usec;
-                gchar *str;
+                g_autofree gchar *str = NULL;
 
                 g_variant_get (timestamp, "(tt)", &sec, &usec);
 
@@ -120,10 +120,8 @@ print_location (GClueSimple *simple)
                 str = g_date_time_format
                       (date_time,
                        "%c (%s seconds since the Epoch)");
-                g_date_time_unref (date_time);
 
                 g_print ("Timestamp:   %s\n", str);
-                g_free (str);
         }
 }
 
diff --git a/interface/fi.w1.wpa_supplicant1.xml b/interface/fi.w1.wpa_supplicant1.xml
index a559c89..5a75c31 100644
--- a/interface/fi.w1.wpa_supplicant1.xml
+++ b/interface/fi.w1.wpa_supplicant1.xml
@@ -2,43 +2,39 @@
 <!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
  "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
 <node>
-
-<interface name="fi.w1.wpa_supplicant1">
-
-<signal name="InterfaceAdded">
-<arg name="path" type="o"/>
-<arg name="properties" type="a{sv}"/>
-</signal>
-<signal name="InterfaceRemoved">
-<arg name="path" type="o"/>
-</signal>
-<property name="Interfaces" type="ao" access="read"/>
-</interface>
-
-<interface name="fi.w1.wpa_supplicant1.Interface">
-<method name="Scan">
-  <arg name="args" type="a{sv}" direction="in"/>
-</method>
-<signal name="BSSAdded">
-<arg name="path" type="o"/>
-<arg name="properties" type="a{sv}"/>
-</signal>
-<signal name="BSSRemoved">
-<arg name="path" type="o"/>
-</signal>
-<signal name="ScanDone">
-<arg name="success" type="b"/>
-</signal>
-<property name="State" type="s" access="read"/>
-<property name="Ifname" type="s" access="read"/>
-<property name="BSSs" type="ao" access="read"/>
-</interface>
-
-<interface name="fi.w1.wpa_supplicant1.BSS">
-<property name="SSID" type="ay" access="read"/>
-<property name="BSSID" type="ay" access="read"/>
-<property name="Signal" type="n" access="read"/>
-<property name="Frequency" type="q" access="read"/>
-</interface>
-
+  <interface name="fi.w1.wpa_supplicant1">
+    <signal name="InterfaceAdded">
+      <arg name="path" type="o"/>
+      <arg name="properties" type="a{sv}"/>
+    </signal>
+    <signal name="InterfaceRemoved">
+      <arg name="path" type="o"/>
+    </signal>
+    <property access="read" name="Interfaces" type="ao"/>
+  </interface>
+  <interface name="fi.w1.wpa_supplicant1.Interface">
+    <method name="Scan">
+      <arg direction="in" name="args" type="a{sv}"/>
+    </method>
+    <signal name="BSSAdded">
+      <arg name="path" type="o"/>
+      <arg name="properties" type="a{sv}"/>
+    </signal>
+    <signal name="BSSRemoved">
+      <arg name="path" type="o"/>
+    </signal>
+    <signal name="ScanDone">
+      <arg name="success" type="b"/>
+    </signal>
+    <property access="read" name="State" type="s"/>
+    <property access="read" name="Ifname" type="s"/>
+    <property access="read" name="BSSs" type="ao"/>
+  </interface>
+  <interface name="fi.w1.wpa_supplicant1.BSS">
+    <property access="read" name="SSID" type="ay"/>
+    <property access="read" name="BSSID" type="ay"/>
+    <property access="read" name="Signal" type="n"/>
+    <property access="read" name="Frequency" type="q"/>
+    <property access="read" name="Age" type="u"/>
+  </interface>
 </node>
diff --git a/libgeoclue/gclue-helpers.c b/libgeoclue/gclue-helpers.c
index bbb3809..de92b38 100644
--- a/libgeoclue/gclue-helpers.c
+++ b/libgeoclue/gclue-helpers.c
@@ -161,7 +161,7 @@ on_get_client_ready (GObject      *source_object,
 {
         GTask *task = G_TASK (user_data);
         GClueManager *manager = GCLUE_MANAGER (source_object);
-        char *client_path;
+        g_autofree char *client_path = NULL;
         GError *error = NULL;
 
         if (!gclue_manager_call_get_client_finish (manager,
@@ -182,7 +182,6 @@ on_get_client_ready (GObject      *source_object,
                                         g_task_get_cancellable (task),
                                         on_client_proxy_ready,
                                         task);
-        g_free (client_path);
         g_object_unref (manager);
 }
 
diff --git a/libgeoclue/gclue-simple.c b/libgeoclue/gclue-simple.c
index 1284e4d..4c2f299 100644
--- a/libgeoclue/gclue-simple.c
+++ b/libgeoclue/gclue-simple.c
@@ -63,14 +63,11 @@ struct _GClueSimplePrivate
         GClueClient *client;
         GClueLocation *location;
 
-        gulong update_id;
-
         GTask *task;
         GCancellable *cancellable;
 
         char *sender;
         XdpLocation *portal;
-        gulong location_updated_id;
         guint response_id;
         char *session_id;
 };
@@ -104,16 +101,11 @@ gclue_simple_finalize (GObject *object)
         GClueSimplePrivate *priv = GCLUE_SIMPLE (object)->priv;
 
         g_clear_pointer (&priv->desktop_id, g_free);
-        if (priv->update_id != 0) {
-                g_signal_handler_disconnect (priv->client, priv->update_id);
-                priv->update_id = 0;
-        }
         if (priv->cancellable != NULL)
                 g_cancellable_cancel (priv->cancellable);
         g_clear_object (&priv->cancellable);
         g_clear_object (&priv->client);
         g_clear_object (&priv->location);
-        g_clear_object (&priv->task);
 
         clear_portal (GCLUE_SIMPLE (object));
 
@@ -296,14 +288,13 @@ on_location_proxy_ready (GObject      *source_object,
                          gpointer      user_data)
 {
         GClueSimplePrivate *priv = GCLUE_SIMPLE (user_data)->priv;
-        GClueLocation *location;
-        GError *error = NULL;
+        g_autoptr (GClueLocation) location = NULL;
+        g_autoptr (GError) error = NULL;
 
         location = gclue_location_proxy_new_for_bus_finish (res, &error);
         if (error != NULL) {
                 if (priv->task != NULL) {
-                        g_task_return_error (priv->task, error);
-                        g_clear_object (&priv->task);
+                        g_task_return_error (priv->task, g_steal_pointer (&error));
                 } else {
                         g_warning ("Failed to create location proxy: %s",
                                    error->message);
@@ -311,12 +302,10 @@ on_location_proxy_ready (GObject      *source_object,
 
                 return;
         }
-        g_clear_object (&priv->location);
-        priv->location = location;
+        g_set_object (&priv->location, location);
 
         if (priv->task != NULL) {
                 g_task_return_boolean (priv->task, TRUE);
-                g_clear_object (&priv->task);
         } else {
                 g_object_notify (G_OBJECT (user_data), "location");
         }
@@ -342,27 +331,46 @@ on_location_updated (GClueClient *client,
                                           user_data);
 }
 
+static void
+async_init_return_error_when_cancelled (GTask *task)
+{
+        GCancellable *cancellable = g_task_get_cancellable (task);
+
+        if (cancellable == NULL)
+                return;
+
+        /* Sub-tasks were returning when the async init is cancelled. With no
+         * further async call, and the top level task only checking cancellation
+         * when g_task_return_* is called, we need to listen to this signal.
+         */
+        g_signal_connect_object (cancellable,
+                                 "cancelled",
+                                 G_CALLBACK (g_task_return_error_if_cancelled),
+                                 task,
+                                 G_CONNECT_SWAPPED);
+}
+
 static void
 on_client_started (GObject      *source_object,
                    GAsyncResult *res,
                    gpointer      user_data)
 {
-        GTask *task = G_TASK (user_data);
+        g_autoptr (GTask) task = G_TASK (user_data);
         GClueClient *client = GCLUE_CLIENT (source_object);
         GClueSimple *simple;
         const char *location;
-        GError *error = NULL;
+        g_autoptr (GError) error = NULL;
 
         simple = g_task_get_source_object (task);
 
         gclue_client_call_start_finish (client, res, &error);
         if (error != NULL) {
-                g_task_return_error (task, error);
-                g_clear_object (&simple->priv->task);
-
+                g_task_return_error (task, g_steal_pointer (&error));
                 return;
         }
 
+        async_init_return_error_when_cancelled (task);
+
         location = gclue_client_get_location (client);
 
         on_location_updated (client, NULL, location, simple);
@@ -373,16 +381,14 @@ on_client_created (GObject      *source_object,
                    GAsyncResult *res,
                    gpointer      user_data)
 {
-        GTask *task = G_TASK (user_data);
+        g_autoptr (GTask) task = G_TASK (user_data);
         GClueSimple *simple = g_task_get_source_object (task);
         GClueSimplePrivate *priv = simple->priv;
-        GError *error = NULL;
+        g_autoptr (GError) error = NULL;
 
         priv->client = gclue_client_proxy_create_full_finish (res, &error);
         if (error != NULL) {
-                g_task_return_error (task, error);
-                g_clear_object (&priv->task);
-
+                g_task_return_error (task, g_steal_pointer (&error));
                 return;
         }
         if (priv->distance_threshold != 0) {
@@ -394,17 +400,19 @@ on_client_created (GObject      *source_object,
                         (priv->client, priv->time_threshold);
         }
 
-        priv->task = task;
-        priv->update_id =
-                g_signal_connect (priv->client,
-                                  "location-updated",
-                                  G_CALLBACK (on_location_updated),
-                                  simple);
+        priv->task = g_steal_pointer (&task);
+        g_object_add_weak_pointer (G_OBJECT (priv->task), (gpointer*) &priv->task);
+
+        g_signal_connect_object (priv->client,
+                                 "location-updated",
+                                 G_CALLBACK (on_location_updated),
+                                 simple,
+                                 G_CONNECT_DEFAULT);
 
         gclue_client_call_start (priv->client,
-                                 g_task_get_cancellable (task),
+                                 g_task_get_cancellable (priv->task),
                                  on_client_started,
-                                 task);
+                                 priv->task);
 }
 
 /* We use the portal if we are inside a flatpak,
@@ -458,7 +466,6 @@ clear_portal (GClueSimple *simple)
         }
 
         g_clear_object (&priv->portal);
-        priv->location_updated_id = 0;
         g_clear_pointer (&priv->session_id, g_free);
         g_clear_pointer (&priv->sender, g_free);
 }
@@ -479,7 +486,7 @@ on_portal_location_updated (XdpLocation *portal,
         double heading;
         const char *description;
         GVariant *timestamp;
-        GClueLocation *location = gclue_location_skeleton_new ();
+        g_autoptr (GClueLocation) location = gclue_location_skeleton_new ();
 
         g_variant_lookup (data, "Latitude", "d", &latitude);
         g_variant_lookup (data, "Longitude", "d", &longitude);
@@ -503,13 +510,10 @@ on_portal_location_updated (XdpLocation *portal,
 
         if (priv->task) {
                 g_task_return_boolean (priv->task, TRUE);
-                g_clear_object (&priv->task);
         }
         else {
                 g_object_notify (G_OBJECT (simple), "location");
         }
-
-        g_object_unref (location);
 }
 
 static void
@@ -532,9 +536,7 @@ on_started (GDBusConnection *bus,
         g_variant_get (parameters, "(u@a{sv})", &response, &ret);
 
         if (response != 0) {
-                clear_portal (simple);
                 g_task_return_new_error (priv->task, G_IO_ERROR, G_IO_ERROR_FAILED, "Start failed");
-                g_clear_object (&priv->task);
         }
 }
 
@@ -543,15 +545,18 @@ on_portal_started_finish (GObject      *source_object,
                           GAsyncResult *res,
                           gpointer      user_data)
 {       
-        GTask *task = G_TASK (user_data);
+        g_autoptr (GTask) task = G_TASK (user_data);
         GClueSimple *simple = g_task_get_source_object (task);
         GClueSimplePrivate *priv = simple->priv;
-        GError *error = NULL;
+        g_autoptr (GError) error = NULL;
 
         if (!xdp_location_call_start_finish (priv->portal, NULL, res, &error)) {
-                clear_portal (simple);
-                g_task_return_new_error (priv->task, G_IO_ERROR, G_IO_ERROR_FAILED, "Start failed");
-                g_clear_object (&priv->task);
+                if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+                        g_task_return_error (task, g_steal_pointer (&error));
+                else
+                        g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_FAILED, "Start failed");
+        } else {
+                async_init_return_error_when_cancelled (task);
         }
 }
 
@@ -560,31 +565,28 @@ on_session_created (GObject *source,
                     GAsyncResult *result,
                     gpointer user_data)
 {
-        GTask *task = G_TASK (user_data);
+        g_autoptr (GTask) task = G_TASK (user_data);
         GClueSimple *simple = g_task_get_source_object (task);
         GClueSimplePrivate *priv = simple->priv;
         GDBusConnection *bus = g_dbus_proxy_get_connection (G_DBUS_PROXY (priv->portal));
-        GError *error = NULL;
+        g_autoptr (GError) error = NULL;
         g_autofree char *handle = NULL;
         g_autofree char *token = NULL;
         g_autofree char *request_path = NULL;
         GVariantBuilder options;
 
         if (!xdp_location_call_create_session_finish (priv->portal, &handle, result, &error)) {
-                clear_portal (simple);
-                g_task_return_error (task, error);
-                g_object_unref (task);
+                g_task_return_error (task, g_steal_pointer (&error));
                 return;
         }
 
         if (!g_str_equal (handle, priv->session_id)) {
-                clear_portal (simple);
                 g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_FAILED, "Unexpected session id");
-                g_object_unref (task);
                 return;
         }
 
-        priv->task = task;
+        priv->task = g_steal_pointer (&task);
+        g_object_add_weak_pointer (G_OBJECT (priv->task), (gpointer*) &priv->task);
 
         token = g_strdup_printf ("geoclue%d", g_random_int_range (0, G_MAXINT));
         request_path = g_strconcat (PORTAL_OBJECT_PATH, "/request/", priv->sender, "/", token, NULL);
@@ -605,9 +607,9 @@ on_session_created (GObject *source,
                                  priv->session_id,
                                  "", /* FIXME parent window */
                                  g_variant_builder_end (&options),
-                                 NULL,
+                                 g_task_get_cancellable (priv->task),
                                  on_portal_started_finish,
-                                 task);
+                                 priv->task);
 }
 
 static int
@@ -638,29 +640,30 @@ on_portal_created (GObject      *source_object,
                    GAsyncResult *res,
                    gpointer      user_data)
 {
-        GTask *task = G_TASK (user_data);
+        g_autoptr (GTask) task = G_TASK (user_data);
         GClueSimple *simple = g_task_get_source_object (task);
         GClueSimplePrivate *priv = simple->priv;
         GDBusConnection *bus;
-        GError *error = NULL;
+        g_autoptr (GError) error = NULL;
         int i;
         g_autofree char *session_token = NULL;
         GVariantBuilder options;
+        GCancellable *cancellable;
 
         priv->portal = xdp_location_proxy_new_for_bus_finish (res, &error);
 
         if (error != NULL) {
-                g_task_return_error (task, error);
-                g_object_unref (task);
+                g_task_return_error (task, g_steal_pointer (&error));
                 return;
         }
 
         bus = g_dbus_proxy_get_connection (G_DBUS_PROXY (priv->portal));
 
-        priv->location_updated_id = g_signal_connect (priv->portal,
-                                                      "location-updated",
-                                                      G_CALLBACK (on_portal_location_updated),
-                                                      simple);
+        g_signal_connect_object (priv->portal,
+                                 "location-updated",
+                                 G_CALLBACK (on_portal_location_updated),
+                                 simple,
+                                 G_CONNECT_DEFAULT);
 
         priv->sender = g_strdup (g_dbus_connection_get_unique_name (bus) + 1);
         for (i = 0; priv->sender[i]; i++)
@@ -678,9 +681,12 @@ on_portal_created (GObject      *source_object,
         g_variant_builder_add (&options, "{sv}", "time-threshold", g_variant_new_uint32 (0));
         g_variant_builder_add (&options, "{sv}", "accuracy", g_variant_new_uint32 (accuracy_level_to_portal (simple->priv->accuracy_level)));
 
+        cancellable = g_task_get_cancellable (task);
         xdp_location_call_create_session (priv->portal,
                                           g_variant_builder_end (&options),
-                                          NULL, on_session_created, task);
+                                          cancellable,
+                                          on_session_created,
+                                          g_steal_pointer (&task));
 }
 
 static void
@@ -702,14 +708,14 @@ gclue_simple_init_async (GAsyncInitable     *initable,
                                                 PORTAL_OBJECT_PATH,
                                                 cancellable,
                                                 on_portal_created,
-                                                task);
+                                                g_object_ref (task));
         } else {
                 gclue_client_proxy_create_full (simple->priv->desktop_id,
                                                 simple->priv->accuracy_level,
                                                 GCLUE_CLIENT_PROXY_CREATE_AUTO_DELETE,
                                                 cancellable,
                                                 on_client_created,
-                                                task);
+                                                g_object_ref (task));
         }
 }
 
@@ -718,7 +724,9 @@ gclue_simple_init_finish (GAsyncInitable *initable,
                           GAsyncResult   *result,
                           GError        **error)
 {
-        return g_task_propagate_boolean (G_TASK (result), error);
+        g_autoptr (GTask) task = G_TASK (result);
+
+        return g_task_propagate_boolean (task, error);
 }
 
 static void
@@ -782,13 +790,13 @@ gclue_simple_new_finish (GAsyncResult *result,
                          GError      **error)
 {
         GObject *object;
-        GObject *source_object;
+        g_autoptr (GObject) source_object = NULL;
 
         source_object = g_async_result_get_source_object (result);
         object = g_async_initable_new_finish (G_ASYNC_INITABLE (source_object),
                                               result,
                                               error);
-        g_object_unref (source_object);
+
         if (object != NULL)
                 return GCLUE_SIMPLE (object);
         else
@@ -857,7 +865,7 @@ gclue_simple_new_sync (const char        *desktop_id,
 {
         GClueSimple *simple;
         GMainLoop *main_loop;
-        GTask *task;
+        g_autoptr (GTask) task = NULL;
 
         task = g_task_new (NULL, cancellable, NULL, NULL);
         main_loop = g_main_loop_new (NULL, FALSE);
@@ -875,8 +883,6 @@ gclue_simple_new_sync (const char        *desktop_id,
 
         simple = g_task_propagate_pointer (task, error);
 
-        g_object_unref (task);
-
         return simple;
 }
 
@@ -941,7 +947,7 @@ gclue_simple_new_with_thresholds_sync (const char        *desktop_id,
 {
         GClueSimple *simple;
         GMainLoop *main_loop;
-        GTask *task;
+        g_autoptr (GTask) task = NULL;
 
         task = g_task_new (NULL, cancellable, NULL, NULL);
         main_loop = g_main_loop_new (NULL, FALSE);
@@ -961,8 +967,6 @@ gclue_simple_new_with_thresholds_sync (const char        *desktop_id,
 
         simple = g_task_propagate_pointer (task, error);
 
-        g_object_unref (task);
-
         return simple;
 }
 
@@ -974,7 +978,7 @@ gclue_simple_new_with_thresholds_sync (const char        *desktop_id,
  * Gets the client proxy, or %NULL if @simple is not using a client proxy (i-e
  * when inside the Flatpak sandbox).
  *
- * Returns: (transfer none) (type GClueClientProxy): The client object.
+ * Returns: (nullable) (transfer none) (type GClueClientProxy): The client object.
  */
 GClueClient *
 gclue_simple_get_client (GClueSimple *simple)
@@ -990,7 +994,7 @@ gclue_simple_get_client (GClueSimple *simple)
  *
  * Gets the current location.
  *
- * Returns: (transfer none) (type GClueLocation): The last known location
+ * Returns: (nullable) (transfer none) (type GClueLocation): The last known location
  * as #GClueLocation.
  */
 GClueLocation *
diff --git a/meson.build b/meson.build
index 8aa5c31..c561572 100644
--- a/meson.build
+++ b/meson.build
@@ -1,4 +1,4 @@
-project('geoclue', 'c', version: '2.6.0', meson_version : '>= 0.47.2')
+project('geoclue', 'c', version: '2.7.2', meson_version : '>= 0.60.0')
 
 gclue_version = meson.project_version()
 ver_arr = gclue_version.split('.')
@@ -27,10 +27,11 @@ conf.set_quoted('PACKAGE_TARNAME', 'geoclue')
 conf.set_quoted('PACKAGE_STRING', 'geoclue ' + gclue_version)
 conf.set_quoted('PACKAGE_URL', 'https://gitlab.freedesktop.org/geoclue/geoclue/wikis/home')
 conf.set_quoted('PACKAGE_BUGREPORT', 'https://gitlab.freedesktop.org/geoclue/geoclue/issues/new')
-conf.set_quoted('TEST_SRCDIR', meson.source_root() + '/data/')
+conf.set_quoted('TEST_SRCDIR', meson.project_source_root() + '/data/')
 conf.set_quoted('LOCALEDIR', localedir)
 conf.set_quoted('SYSCONFDIR', sysconfdir)
-conf.set_quoted('MOZILLA_API_KEY', get_option('mozilla-api-key'))
+conf.set_quoted('DEFAULT_WIFI_URL', get_option('default-wifi-url'))
+conf.set_quoted('DEFAULT_WIFI_SUBMIT_URL', get_option('default-wifi-submit-url'))
 conf.set10('GCLUE_USE_3G_SOURCE', get_option('3g-source'))
 conf.set10('GCLUE_USE_CDMA_SOURCE', get_option('cdma-source'))
 conf.set10('GCLUE_USE_MODEM_GPS_SOURCE', get_option('modem-gps-source'))
@@ -43,9 +44,9 @@ configinc = include_directories('.')
 gnome = import('gnome')
 cc = meson.get_compiler('c')
 
-base_deps = [ dependency('glib-2.0', version: '>= 2.44.0'),
-              dependency('gio-2.0', version: '>= 2.44.0'),
-              dependency('gio-unix-2.0', version: '>= 2.44.0') ]
+base_deps = [ dependency('glib-2.0', version: '>= 2.74.0'),
+              dependency('gio-2.0', version: '>= 2.74.0'),
+              dependency('gio-unix-2.0', version: '>= 2.74.0') ]
 libm = cc.find_library('m', required: false)
 if libm.found()
     base_deps += [ libm ]
diff --git a/meson_options.txt b/meson_options.txt
index 5b8c42d..4d55a63 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -40,6 +40,9 @@ option('systemd-system-unit-dir',
 option('dbus-srv-user',
        type: 'string', value: 'root',
        description: 'The user (existing) as which the service will run')
-option('mozilla-api-key',
-       type: 'string', value: 'geoclue',
-       description: 'Your API key for Mozilla Location Service')
+option('default-wifi-url',
+       type: 'string',
+       description: 'Default URL for Ichnaea-compatible WiFi geolocation')
+option('default-wifi-submit-url',
+       type: 'string',
+       description: 'Default URL for Ichnaea-compatible WiFi location submission')
diff --git a/src/gclue-3g-tower.h b/src/gclue-3g-tower.h
index 4333245..f6b69ff 100644
--- a/src/gclue-3g-tower.h
+++ b/src/gclue-3g-tower.h
@@ -26,9 +26,12 @@ G_BEGIN_DECLS
 
 typedef enum {
   GCLUE_TOWER_TEC_UNKNOWN = 0,
-  GCLUE_TOWER_TEC_3G = 1,
-  GCLUE_TOWER_TEC_4G = 2,
+  GCLUE_TOWER_TEC_2G = 1,
+  GCLUE_TOWER_TEC_3G = 2,
+  GCLUE_TOWER_TEC_4G = 3,
+  GCLUE_TOWER_TEC_NO_FIX = 99,
 } GClueTowerTec;
+# define GCLUE_TOWER_TEC_MAX_VALID GCLUE_TOWER_TEC_4G
 
 typedef struct _GClue3GTower GClue3GTower;
 
diff --git a/src/gclue-3g.c b/src/gclue-3g.c
index bbcb791..225b706 100644
--- a/src/gclue-3g.c
+++ b/src/gclue-3g.c
@@ -24,9 +24,11 @@
 #include <libsoup/soup.h>
 #include <string.h>
 #include "gclue-3g.h"
+#include "gclue-3g-tower.h"
 #include "gclue-modem-manager.h"
 #include "gclue-location.h"
 #include "gclue-mozilla.h"
+#include "gclue-wifi.h"
 
 /**
  * SECTION:gclue-3g
@@ -35,14 +37,24 @@
  * Contains functions to get the geolocation based on 3GPP cell towers.
  **/
 
+/* Should be slightly less than MAX_LOCATION_AGE in gclue-locator.c, so we don't
+ * get replaced by a less accurate WiFi location while still connected to a tower.
+ * Technically, this can only happen on the NEIGHBORHOOD accuracy level (since at
+ * this level WiFi does scrambling), but it won't hurt on higher ones, too.
+ * In seconds.
+ */
+#define LOCATION_3GPP_TIMEOUT (25 * 60)
+
+static unsigned int gclue_3g_running;
+
 struct _GClue3GPrivate {
+        GClueMozilla *mozilla;
         GClueModem *modem;
 
         GCancellable *cancellable;
 
         gulong threeg_notify_id;
-
-        GClue3GTower *tower;
+        guint location_3gpp_timeout_id;
 };
 
 G_DEFINE_TYPE_WITH_CODE (GClue3G,
@@ -56,6 +68,7 @@ static GClueLocationSourceStopResult
 gclue_3g_stop (GClueLocationSource *source);
 static SoupMessage *
 gclue_3g_create_query (GClueWebSource *web,
+                       const char **query_data_description,
                        GError        **error);
 static SoupMessage *
 gclue_3g_create_submit_query (GClueWebSource  *web,
@@ -64,24 +77,21 @@ gclue_3g_create_submit_query (GClueWebSource  *web,
 static GClueAccuracyLevel
 gclue_3g_get_available_accuracy_level (GClueWebSource *web,
                                        gboolean available);
-static GClueLocation *
-gclue_3g_parse_response (GClueWebSource *web,
-                         const char     *xml,
-                         GError        **error);
 
 static void
 on_3g_enabled (GObject      *source_object,
                GAsyncResult *result,
                gpointer      user_data)
 {
-        GClue3G *source = GCLUE_3G (user_data);
-        GError *error = NULL;
+        g_autoptr(GError) error = NULL;
 
-        if (!gclue_modem_enable_3g_finish (source->priv->modem,
+        if (!gclue_modem_enable_3g_finish (GCLUE_MODEM (source_object),
                                            result,
                                            &error)) {
-                g_warning ("Failed to enable 3GPP: %s", error->message);
-                g_error_free (error);
+                if (error && !g_error_matches (error, G_IO_ERROR,
+                                               G_IO_ERROR_CANCELLED)) {
+                        g_warning ("Failed to enable 3GPP: %s", error->message);
+                }
         }
 }
 
@@ -92,23 +102,30 @@ on_is_3g_available_notify (GObject    *gobject,
 {
         GClue3G *source = GCLUE_3G (user_data);
         GClue3GPrivate *priv = source->priv;
+        gboolean available_3g;
+
+        available_3g = gclue_modem_get_is_3g_available (priv->modem);
+        g_debug ("3G available notify %d", (int)available_3g);
 
         gclue_web_source_refresh (GCLUE_WEB_SOURCE (source));
 
         if (gclue_location_source_get_active (GCLUE_LOCATION_SOURCE (source)) &&
-            gclue_modem_get_is_3g_available (priv->modem))
+            available_3g)
                 gclue_modem_enable_3g (priv->modem,
                                        priv->cancellable,
                                        on_3g_enabled,
                                        source);
 }
 
-static GClueLocation *
-gclue_3g_parse_response (GClueWebSource *web,
-                         const char     *content,
-                         GError        **error)
+static void cancel_location_3gpp_timeout (GClue3G *g3g)
 {
-        return gclue_mozilla_parse_response (content, error);
+        GClue3GPrivate *priv = g3g->priv;
+
+        if (!priv->location_3gpp_timeout_id)
+                return;
+
+        g_source_remove (priv->location_3gpp_timeout_id);
+        priv->location_3gpp_timeout_id = 0;
 }
 
 static void
@@ -125,7 +142,10 @@ gclue_3g_finalize (GObject *g3g)
                                      priv->threeg_notify_id);
         priv->threeg_notify_id = 0;
 
+        cancel_location_3gpp_timeout (source);
+
         g_clear_object (&priv->modem);
+        g_clear_object (&priv->mozilla);
         g_clear_object (&priv->cancellable);
 }
 
@@ -142,7 +162,6 @@ gclue_3g_class_init (GClue3GClass *klass)
         source_class->stop = gclue_3g_stop;
         web_class->create_query = gclue_3g_create_query;
         web_class->create_submit_query = gclue_3g_create_submit_query;
-        web_class->parse_response = gclue_3g_parse_response;
         web_class->get_available_accuracy_level =
                 gclue_3g_get_available_accuracy_level;
 }
@@ -151,18 +170,26 @@ static void
 gclue_3g_init (GClue3G *source)
 {
         GClue3GPrivate *priv;
+        GClueWebSource *web_source = GCLUE_WEB_SOURCE (source);
 
         source->priv = gclue_3g_get_instance_private (source);
         priv = source->priv;
 
         priv->cancellable = g_cancellable_new ();
 
+        priv->mozilla = gclue_mozilla_get_singleton ();
+        gclue_web_source_set_locate_url (web_source,
+                                         gclue_mozilla_get_locate_url (priv->mozilla));
+        gclue_web_source_set_submit_url (web_source,
+                                         gclue_mozilla_get_submit_url (priv->mozilla));
+
         priv->modem = gclue_modem_manager_get_singleton ();
         priv->threeg_notify_id =
                         g_signal_connect (priv->modem,
                                           "notify::is-3g-available",
                                           G_CALLBACK (on_is_3g_available_notify),
                                           source);
+        priv->location_3gpp_timeout_id = 0;
 }
 
 static void
@@ -177,36 +204,53 @@ on_3g_destroyed (gpointer data,
 /**
  * gclue_3g_new:
  *
- * Get the #GClue3G singleton.
+ * Get the #GClue3G singleton, for the specified max accuracy level @level.
  *
  * Returns: (transfer full): a new ref to #GClue3G. Use g_object_unref()
  * when done.
  **/
 GClue3G *
-gclue_3g_get_singleton (void)
+gclue_3g_get_singleton (GClueAccuracyLevel level)
 {
-        static GClue3G *source = NULL;
-
-        if (source == NULL) {
-                source = g_object_new (GCLUE_TYPE_3G,
-                                       "compute-movement", FALSE,
-                                       NULL);
-                g_object_weak_ref (G_OBJECT (source),
+        static GClue3G *source[] = { NULL, NULL };
+        int i;
+
+        g_return_val_if_fail (level >= GCLUE_ACCURACY_LEVEL_CITY, NULL);
+
+        i = gclue_wifi_should_skip_bsss (level) ? 0 : 1;
+        if (source[i] == NULL) {
+                source[i] = g_object_new (GCLUE_TYPE_3G,
+                                          "accuracy-level", level,
+                                          "compute-movement", FALSE,
+                                          NULL);
+                g_object_weak_ref (G_OBJECT (source[i]),
                                    on_3g_destroyed,
-                                   &source);
+                                   &source[i]);
         } else
-                g_object_ref (source);
+                g_object_ref (source[i]);
+
+        return source[i];
+}
+
+static gboolean
+g3g_should_skip_bsss (GClue3G *g3g)
+{
+        GClueAccuracyLevel level;
 
-        return source;
+        g_object_get (G_OBJECT (g3g), "accuracy-level", &level, NULL);
+        return gclue_wifi_should_skip_bsss (level);
 }
 
 static SoupMessage *
 gclue_3g_create_query (GClueWebSource *web,
+                       const char **query_data_description,
                        GError        **error)
 {
-        GClue3GPrivate *priv = GCLUE_3G (web)->priv;
+        GClue3G *g3g = GCLUE_3G (web);
+        GClue3GPrivate *priv = g3g->priv;
+        gboolean skip_bss;
 
-        if (priv->tower == NULL) {
+        if (!gclue_mozilla_has_tower (priv->mozilla)) {
                 g_set_error_literal (error,
                                      G_IO_ERROR,
                                      G_IO_ERROR_NOT_INITIALIZED,
@@ -214,7 +258,13 @@ gclue_3g_create_query (GClueWebSource *web,
                 return NULL; /* Not initialized yet */
         }
 
-        return gclue_mozilla_create_query (NULL, priv->tower, error);
+        skip_bss = g3g_should_skip_bsss (g3g);
+        if (skip_bss) {
+                g_debug ("Will skip BSSs in query due to our accuracy level");
+        }
+
+        return gclue_mozilla_create_query (priv->mozilla, FALSE, skip_bss,
+                                           query_data_description, error);
 }
 
 static SoupMessage *
@@ -224,7 +274,7 @@ gclue_3g_create_submit_query (GClueWebSource  *web,
 {
         GClue3GPrivate *priv = GCLUE_3G (web)->priv;
 
-        if (priv->tower == NULL) {
+        if (!gclue_mozilla_has_tower (priv->mozilla)) {
                 g_set_error_literal (error,
                                      G_IO_ERROR,
                                      G_IO_ERROR_NOT_INITIALIZED,
@@ -232,9 +282,8 @@ gclue_3g_create_submit_query (GClueWebSource  *web,
                 return NULL; /* Not initialized yet */
         }
 
-        return gclue_mozilla_create_submit_query (location,
-                                                  NULL,
-                                                  priv->tower,
+        return gclue_mozilla_create_submit_query (priv->mozilla,
+                                                  location,
                                                   error);
 }
 
@@ -249,6 +298,41 @@ gclue_3g_get_available_accuracy_level (GClueWebSource *web,
                 return GCLUE_ACCURACY_LEVEL_NONE;
 }
 
+gboolean gclue_3g_should_skip_tower (GClueAccuracyLevel level)
+{
+        return level < GCLUE_ACCURACY_LEVEL_NEIGHBORHOOD;
+}
+
+static gboolean
+on_location_3gpp_timeout (gpointer user_data)
+{
+        GClue3G *g3g = GCLUE_3G (user_data);
+        GClue3GPrivate *priv = g3g->priv;
+
+        if (!gclue_mozilla_has_tower (priv->mozilla)) {
+                g_debug ("3GPP location timeout, but no tower");
+                priv->location_3gpp_timeout_id = 0;
+                return G_SOURCE_REMOVE;
+        }
+
+        g_debug ("3GPP location timeout, re-sending existing location");
+        gclue_web_source_refresh (GCLUE_WEB_SOURCE (g3g));
+
+        return G_SOURCE_CONTINUE;
+}
+
+static void set_location_3gpp_timeout (GClue3G *g3g)
+{
+        GClue3GPrivate *priv = g3g->priv;
+
+        g_debug ("Scheduling new 3GPP location timeout");
+
+        cancel_location_3gpp_timeout (g3g);
+        priv->location_3gpp_timeout_id = g_timeout_add_seconds (LOCATION_3GPP_TIMEOUT,
+                                                                on_location_3gpp_timeout,
+                                                                g3g);
+}
+
 static void
 on_fix_3g (GClueModem   *modem,
            const gchar  *opc,
@@ -257,15 +341,26 @@ on_fix_3g (GClueModem   *modem,
            GClueTowerTec tec,
            gpointer    user_data)
 {
-        GClue3GPrivate *priv = GCLUE_3G (user_data)->priv;
-
-        if (priv->tower == NULL)
-                priv->tower = g_slice_new0 (GClue3GTower);
-        g_strlcpy (priv->tower->opc, opc,
-                   GCLUE_3G_TOWER_OPERATOR_CODE_STR_LEN + 1);
-        priv->tower->lac = lac;
-        priv->tower->cell_id = cell_id;
-        priv->tower->tec = tec;
+        GClue3G *g3g = GCLUE_3G (user_data);
+        GClue3GPrivate *priv = g3g->priv;
+
+        g_debug ("3GPP %s fix available",
+                 tec == GCLUE_TOWER_TEC_NO_FIX ? "no" : "new");
+
+        if (tec != GCLUE_TOWER_TEC_NO_FIX) {
+                GClue3GTower tower;
+
+                g_strlcpy (tower.opc, opc,
+                           GCLUE_3G_TOWER_OPERATOR_CODE_STR_LEN + 1);
+                tower.lac = lac;
+                tower.cell_id = cell_id;
+                tower.tec = tec;
+                set_location_3gpp_timeout (g3g);
+                gclue_mozilla_set_tower (priv->mozilla, &tower);
+        } else {
+                cancel_location_3gpp_timeout (g3g);
+                gclue_mozilla_set_tower (priv->mozilla, NULL);
+        }
 
         gclue_web_source_refresh (GCLUE_WEB_SOURCE (user_data));
 }
@@ -286,16 +381,17 @@ gclue_3g_start (GClueLocationSource *source)
         if (base_result != GCLUE_LOCATION_SOURCE_START_RESULT_OK)
                 return base_result;
 
-        if (priv->tower != NULL) {
-                g_slice_free (GClue3GTower, priv->tower);
-                priv->tower = NULL;
+        if (gclue_3g_running == 0) {
+                g_debug ("First 3GPP source starting up");
         }
+        gclue_3g_running++;
 
         g_signal_connect (priv->modem,
                           "fix-3g",
                           G_CALLBACK (on_fix_3g),
                           source);
 
+        /* Emits fix-3g signal even if the location hasn't actually changed to prime us */
         if (gclue_modem_get_is_3g_available (priv->modem))
                 gclue_modem_enable_3g (priv->modem,
                                        priv->cancellable,
@@ -307,30 +403,42 @@ gclue_3g_start (GClueLocationSource *source)
 static GClueLocationSourceStopResult
 gclue_3g_stop (GClueLocationSource *source)
 {
-        GClue3GPrivate *priv = GCLUE_3G (source)->priv;
+        GClue3G *g3g = GCLUE_3G (source);
+        GClue3GPrivate *priv = g3g->priv;
         GClueLocationSourceClass *base_class;
-        GError *error = NULL;
+        g_autoptr(GError) error = NULL;
         GClueLocationSourceStopResult base_result;
 
         g_return_val_if_fail (GCLUE_IS_LOCATION_SOURCE (source), FALSE);
 
         base_class = GCLUE_LOCATION_SOURCE_CLASS (gclue_3g_parent_class);
         base_result = base_class->stop (source);
-        if (base_result == GCLUE_LOCATION_SOURCE_STOP_RESULT_STILL_USED)
+        if (base_result != GCLUE_LOCATION_SOURCE_STOP_RESULT_OK)
                 return base_result;
 
         g_signal_handlers_disconnect_by_func (G_OBJECT (priv->modem),
                                               G_CALLBACK (on_fix_3g),
                                               source);
 
+        cancel_location_3gpp_timeout (g3g);
+
+        g_assert (gclue_3g_running > 0);
+        gclue_3g_running--;
+        if (gclue_3g_running > 0) {
+                return base_result;
+        }
+
+        g_debug ("Last 3GPP source stopping, disabling location gathering and invalidating existing tower");
+
         if (gclue_modem_get_is_3g_available (priv->modem))
                 if (!gclue_modem_disable_3g (priv->modem,
                                              priv->cancellable,
                                              &error)) {
                         g_warning ("Failed to disable 3GPP: %s",
                                    error->message);
-                        g_error_free (error);
                 }
 
+        gclue_mozilla_set_tower (priv->mozilla, NULL);
+
         return base_result;
 }
diff --git a/src/gclue-3g.h b/src/gclue-3g.h
index bec215b..0eacf88 100644
--- a/src/gclue-3g.h
+++ b/src/gclue-3g.h
@@ -22,6 +22,8 @@
 #ifndef GCLUE_3G_H
 #define GCLUE_3G_H
 
+#include "config.h"
+
 #include <glib.h>
 #include <gio/gio.h>
 #include "gclue-web-source.h"
@@ -62,7 +64,13 @@ struct _GClue3GClass {
         GClueWebSourceClass parent_class;
 };
 
-GClue3G * gclue_3g_get_singleton (void);
+GClue3G * gclue_3g_get_singleton (GClueAccuracyLevel level);
+
+#if GCLUE_USE_3G_SOURCE
+gboolean gclue_3g_should_skip_tower (GClueAccuracyLevel level);
+#else
+static inline gboolean gclue_3g_should_skip_tower (GClueAccuracyLevel level) { return TRUE; }
+#endif
 
 G_END_DECLS
 
diff --git a/src/gclue-cdma.c b/src/gclue-cdma.c
index a35c07a..75bd5a0 100644
--- a/src/gclue-cdma.c
+++ b/src/gclue-cdma.c
@@ -79,14 +79,15 @@ on_cdma_enabled (GObject      *source_object,
                  GAsyncResult *result,
                  gpointer      user_data)
 {
-        GClueCDMA *source = GCLUE_CDMA (user_data);
-        GError *error = NULL;
+        g_autoptr(GError) error = NULL;
 
-        if (!gclue_modem_enable_cdma_finish (source->priv->modem,
+        if (!gclue_modem_enable_cdma_finish (GCLUE_MODEM (source_object),
                                              result,
                                              &error)) {
-                g_warning ("Failed to enable CDMA: %s", error->message);
-                g_error_free (error);
+                if (error && !g_error_matches (error, G_IO_ERROR,
+                                                G_IO_ERROR_CANCELLED)) {
+                        g_warning ("Failed to enable CDMA: %s", error->message);
+                }
         }
 }
 
@@ -199,7 +200,8 @@ on_fix_cdma (GClueModem *modem,
 
         location = gclue_location_new (latitude,
                                        longitude,
-                                       1000);     /* Assume 1 km accuracy */
+                                       1000, /* Assume 1 km accuracy */
+                                       "CDMA");
 
         gclue_location_source_set_location (GCLUE_LOCATION_SOURCE (user_data),
                                             location);
@@ -241,7 +243,7 @@ gclue_cdma_stop (GClueLocationSource *source)
 {
         GClueCDMAPrivate *priv = GCLUE_CDMA (source)->priv;
         GClueLocationSourceClass *base_class;
-        GError *error = NULL;
+        g_autoptr(GError) error = NULL;
         GClueLocationSourceStopResult base_result;
 
         g_return_val_if_fail (GCLUE_IS_LOCATION_SOURCE (source), FALSE);
@@ -261,7 +263,6 @@ gclue_cdma_stop (GClueLocationSource *source)
                                                &error)) {
                         g_warning ("Failed to disable CDMA: %s",
                                    error->message);
-                        g_error_free (error);
                 }
 
         return base_result;
diff --git a/src/gclue-client-info.c b/src/gclue-client-info.c
index 7fc2ac7..623aeb1 100644
--- a/src/gclue-client-info.c
+++ b/src/gclue-client-info.c
@@ -233,9 +233,9 @@ get_xdg_id (guint32 pid)
                 return NULL;
         lines =  g_strsplit (content, "\n", -1);
 
-	xdg_id = parse_cgroup_v2 (lines);
-	if (xdg_id != NULL)
-		return xdg_id;
+        xdg_id = parse_cgroup_v2 (lines);
+        if (xdg_id != NULL)
+                return xdg_id;
 
         for (i = 0; lines[i] != NULL; i++) {
                 const char *unit = lines[i] + strlen ("1:name=systemd:");
@@ -356,15 +356,18 @@ on_dbus_proxy_ready (GObject      *source_object,
         GTask *task = G_TASK (user_data);
         gpointer *info = g_task_get_source_object (task);
         GClueClientInfoPrivate *priv = GCLUE_CLIENT_INFO (info)->priv;
+        GDBusProxy *dbus_proxy;
         GError *error = NULL;
 
-        priv->dbus_proxy = g_dbus_proxy_new_for_bus_finish (res, &error);
-        if (priv->dbus_proxy == NULL) {
+        dbus_proxy = g_dbus_proxy_new_for_bus_finish (res, &error);
+        if (dbus_proxy == NULL) {
                 g_task_return_error (task, error);
                 g_object_unref (task);
                 return;
         }
 
+        priv->dbus_proxy = dbus_proxy;
+
         g_dbus_proxy_call (priv->dbus_proxy,
                            "GetConnectionUnixUser",
                            g_variant_new ("(s)", priv->bus_name),
diff --git a/src/gclue-compass.c b/src/gclue-compass.c
index f425aac..84b80d1 100644
--- a/src/gclue-compass.c
+++ b/src/gclue-compass.c
@@ -79,14 +79,13 @@ gclue_compass_finalize (GObject *object)
         g_clear_object (&priv->cancellable);
 
         if (priv->proxy != NULL) {
-                GError *error = NULL;
+                g_autoptr(GError) error = NULL;
 
                 if (!compass_call_release_compass_sync (priv->proxy,
                                                         NULL,
                                                         &error)) {
                         g_warning ("Failed to release compass: %s",
                                    error->message);
-                        g_error_free (error);
                 }
                 g_debug ("IIO compass released");
                 g_object_unref (priv->proxy);
@@ -141,15 +140,16 @@ on_compass_claimed (GObject      *source_object,
 {
         GClueCompass *compass;
         Compass *proxy = COMPASS (source_object);
-        GError *error = NULL;
+        g_autoptr(GError) error = NULL;
 
         if (!compass_call_claim_compass_finish (proxy, res, &error)) {
-                if (error->code != G_IO_ERROR_CANCELLED)
+                if (error && !g_error_matches (error, G_IO_ERROR,
+                                               G_IO_ERROR_CANCELLED)) {
                         g_debug ("Failed to claim IIO proxy compass: %s",
                                  error->message);
-                g_error_free (error);
-                g_object_unref (proxy);
+                }
 
+                g_object_unref (proxy);
                 return;
         }
         g_debug ("IIO compass claimed");
@@ -173,21 +173,22 @@ on_compass_proxy_ready (GObject      *source_object,
 {
         GClueCompass *compass;
         Compass *proxy;
-        GError *error = NULL;
+        g_autoptr(GError) error = NULL;
 
         proxy = compass_proxy_new_for_bus_finish (res, &error);
         if (proxy == NULL) {
-                if (error->code != G_IO_ERROR_CANCELLED)
+                if (error && !g_error_matches (error, G_IO_ERROR,
+                                               G_IO_ERROR_CANCELLED)) {
                         g_debug ("Failed to connect to IIO compass proxy: %s",
                                  error->message);
-                g_error_free (error);
+                }
 
                 return;
         }
 
         compass = GCLUE_COMPASS (user_data);
 
-        compass_call_claim_compass (proxy, 
+        compass_call_claim_compass (proxy,
                                     compass->priv->cancellable,
                                     on_compass_claimed,
                                     compass);
diff --git a/src/gclue-config.c b/src/gclue-config.c
index 8c1cc3d..acd6009 100644
--- a/src/gclue-config.c
+++ b/src/gclue-config.c
@@ -22,10 +22,12 @@
 
 #include <glib/gi18n.h>
 #include <config.h>
+#include <string.h>
 
 #include "gclue-config.h"
 
 #define CONFIG_FILE_PATH SYSCONFDIR "/geoclue/geoclue.conf"
+#define CONFIG_D_DIRECTORY SYSCONFDIR "/geoclue/conf.d/"
 
 /* This class will be responsible for fetching configuration. */
 
@@ -44,6 +46,7 @@ struct _GClueConfigPrivate
         gboolean enable_modem_gps_source;
         gboolean enable_wifi_source;
         gboolean enable_compass;
+        gboolean enable_static_source;
         char *wifi_submit_url;
         char *wifi_submit_nick;
         char *nmea_socket;
@@ -101,22 +104,99 @@ gclue_config_class_init (GClueConfigClass *klass)
         object_class->finalize = gclue_config_finalize;
 }
 
-static void
-load_agent_config (GClueConfig *config)
+static gboolean
+load_boolean_value (GClueConfig *config,
+                    const gchar *group_name,
+                    const gchar *key,
+                    gboolean    *value_storage)
 {
         GClueConfigPrivate *priv = config->priv;
-        GError *error = NULL;
 
-        priv->agents = g_key_file_get_string_list (priv->key_file,
-                                                   "agent",
-                                                   "whitelist",
-                                                   &priv->num_agents,
-                                                   &error);
-        if (error != NULL) {
-                g_critical ("Failed to read 'agent/whitelist' key: %s",
-                            error->message);
-                g_error_free (error);
+        g_return_val_if_fail (value_storage != NULL, FALSE);
+
+        if (g_key_file_has_key (priv->key_file, group_name, key, NULL)) {
+                g_autoptr(GError) error = NULL;
+                gboolean value =
+                        g_key_file_get_boolean (priv->key_file,
+                                                group_name, key,
+                                                &error);
+                if (error == NULL) {
+                        *value_storage = value;
+                        return TRUE;
+                } else
+                        g_warning ("Failed to get config \"%s/%s\": %s",
+                                   group_name, key, error->message);
         }
+
+        return FALSE;
+}
+
+static gboolean
+load_string_value (GClueConfig  *config,
+                   const gchar  *group_name,
+                   const gchar  *key,
+                   gchar       **value_storage)
+{
+        GClueConfigPrivate *priv = config->priv;
+
+        g_return_val_if_fail (value_storage != NULL, FALSE);
+
+        if (g_key_file_has_key (priv->key_file, group_name, key, NULL)) {
+                g_autoptr(GError) error = NULL;
+                g_autofree gchar *value =
+                        g_key_file_get_string (priv->key_file,
+                                               group_name, key,
+                                               &error);
+                if (error == NULL) {
+                        g_clear_pointer (value_storage, g_free);
+                        *value_storage = g_steal_pointer (&value);
+                        return TRUE;
+                } else
+                        g_warning ("Failed to get config \"%s/%s\": %s",
+                                   group_name, key, error->message);
+        }
+
+        return FALSE;
+}
+
+static gboolean
+load_string_list_value (GClueConfig  *config,
+                        const gchar  *group_name,
+                        const gchar  *key,
+                        GStrv        *value_storage,
+                        gsize        *length_storage)
+{
+        GClueConfigPrivate *priv = config->priv;
+
+        g_return_val_if_fail (value_storage != NULL, FALSE);
+        g_return_val_if_fail (length_storage != NULL, FALSE);
+
+        if (g_key_file_has_key (priv->key_file, group_name, key, NULL)) {
+                g_autoptr(GError) error = NULL;
+                gsize length = 0;
+                g_auto(GStrv) value =
+                        g_key_file_get_string_list (priv->key_file,
+                                                    group_name, key,
+                                                    &length, &error);
+                if (error == NULL) {
+                        g_clear_pointer (value_storage, g_strfreev);
+                        *value_storage = g_steal_pointer (&value);
+                        *length_storage = length;
+                        return TRUE;
+                } else
+                        g_warning ("Failed to get config \"%s/%s\": %s",
+                                   group_name, key, error->message);
+        }
+
+        return FALSE;
+}
+
+static void
+load_agent_config (GClueConfig *config)
+{
+        load_string_list_value (config, "agent", "whitelist",
+                                &config->priv->agents,
+                                &config->priv->num_agents);
 }
 
 static void
@@ -124,221 +204,193 @@ load_app_configs (GClueConfig *config)
 {
         const char *known_groups[] = { "agent", "wifi", "3g", "cdma",
                                        "modem-gps", "network-nmea", "compass",
-                                       NULL };
+                                       "static-source", NULL };
         GClueConfigPrivate *priv = config->priv;
         gsize num_groups = 0, i;
-        char **groups;
+        g_auto(GStrv) groups = NULL;
 
         groups = g_key_file_get_groups (priv->key_file, &num_groups);
         if (num_groups == 0)
                 return;
 
         for (i = 0; i < num_groups; i++) {
-                AppConfig *app_config;
-                int* users;
+                AppConfig *app_config = NULL;
+                g_autofree int *users = NULL;
+                GList *node;
                 gsize num_users = 0, j;
                 gboolean allowed, system;
                 gboolean ignore = FALSE;
-                GError *error = NULL;
+                gboolean new_app_config = TRUE;
+                gboolean has_allowed = FALSE;
+                gboolean has_system = FALSE;
+                gboolean has_users = FALSE;
+                g_autoptr(GError) error = NULL;
 
                 for (j = 0; known_groups[j] != NULL; j++)
                         if (strcmp (groups[i], known_groups[j]) == 0) {
                                 ignore = TRUE;
 
-                                continue;
+                                break;
                         }
 
                 if (ignore)
                         continue;
 
+                /* Check if entry is new or is overwritten */
+                for (node = priv->app_configs; node != NULL; node = node->next) {
+                        if (strcmp (((AppConfig *) node->data)->id, groups[i]) == 0) {
+                                app_config = (AppConfig *) node->data;
+                                new_app_config = FALSE;
+
+                                break;
+                        }
+                }
+
                 allowed = g_key_file_get_boolean (priv->key_file,
                                                   groups[i],
                                                   "allowed",
                                                   &error);
-                if (error != NULL)
+                has_allowed = (error == NULL);
+                if (error != NULL && new_app_config)
                         goto error_out;
+                g_clear_error (&error);
 
                 system = g_key_file_get_boolean (priv->key_file,
                                                  groups[i],
                                                  "system",
                                                  &error);
-                if (error != NULL)
+                has_system = (error == NULL);
+                if (error != NULL && new_app_config)
                         goto error_out;
+                g_clear_error (&error);
 
                 users = g_key_file_get_integer_list (priv->key_file,
                                                      groups[i],
                                                      "users",
                                                      &num_users,
                                                      &error);
-                if (error != NULL)
+                has_users = (error == NULL);
+                if (error != NULL && new_app_config)
                         goto error_out;
+                g_clear_error (&error);
+
+
+                /* New app config, without erroring out above */
+                if (new_app_config) {
+                        app_config = g_slice_new0 (AppConfig);
+                        priv->app_configs = g_list_prepend (priv->app_configs, app_config);
+                        app_config->id = g_strdup (groups[i]);
+                }
 
-                app_config = g_slice_new0 (AppConfig);
-                app_config->id = g_strdup (groups[i]);
-                app_config->allowed = allowed;
-                app_config->system = system;
-                app_config->users = users;
-                app_config->num_users = num_users;
+                /* New app configs will have all of them, overwrites only some */
+                if (has_allowed)
+                        app_config->allowed = allowed;
 
-                priv->app_configs = g_list_prepend (priv->app_configs, app_config);
+                if (has_system)
+                        app_config->system = system;
+
+                if (has_users) {
+                        g_free (app_config->users);
+                        app_config->users = g_steal_pointer (&users);
+                        app_config->num_users = num_users;
+                }
 
                 continue;
 error_out:
                 g_warning ("Failed to load configuration for app '%s': %s",
                            groups[i],
                            error->message);
-                g_error_free (error);
         }
-
-        g_strfreev (groups);
 }
 
-static gboolean
-load_enable_source_config (GClueConfig *config,
-                           const char  *source_name)
-{
-        GClueConfigPrivate *priv = config->priv;
-        GError *error = NULL;
-        gboolean enable;
-
-        enable = g_key_file_get_boolean (priv->key_file,
-                                         source_name,
-                                         "enable",
-                                         &error);
-        if (error != NULL) {
-                g_debug ("Failed to get config %s/enable:"
-                         " %s",
-                         source_name,
-                         error->message);
-                g_error_free (error);
-
-                /* Source should be enabled by default */
-                return TRUE;
-        }
-
-        return enable;
-}
-
-#define DEFAULT_WIFI_URL "https://location.services.mozilla.com/v1/geolocate?key=" MOZILLA_API_KEY
-#define DEFAULT_WIFI_SUBMIT_URL "https://location.services.mozilla.com/v1/submit?key=" MOZILLA_API_KEY
 #define DEFAULT_WIFI_SUBMIT_NICK "geoclue"
 
 static void
 load_wifi_config (GClueConfig *config)
 {
         GClueConfigPrivate *priv = config->priv;
-        GError *error = NULL;
+        g_autofree gchar *wifi_submit_nick = NULL;
 
-        priv->enable_wifi_source = load_enable_source_config (config, "wifi");
+        load_boolean_value (config, "wifi", "enable",
+                            &priv->enable_wifi_source);
 
-        priv->wifi_url = g_key_file_get_string (priv->key_file,
-                                                "wifi",
-                                                "url",
-                                                &error);
-        if (error != NULL) {
-                g_debug ("Failed to get config \"wifi/url\": %s",
-                         error->message);
-                g_clear_error (&error);
-                priv->wifi_url = g_strdup (DEFAULT_WIFI_URL);
-        }
+        load_string_value (config, "wifi", "url", &priv->wifi_url);
 
-        priv->wifi_submit = g_key_file_get_boolean (priv->key_file,
-                                                    "wifi",
-                                                    "submit-data",
-                                                    &error);
-        if (error != NULL) {
-                g_debug ("Failed to get config \"wifi/submit-data\": %s",
-                         error->message);
-                g_error_free (error);
+        load_boolean_value (config, "wifi", "submit-data", &priv->wifi_submit);
 
-                return;
-        }
-
-        priv->wifi_submit_url = g_key_file_get_string (priv->key_file,
-                                                       "wifi",
-                                                       "submission-url",
-                                                       &error);
-        if (error != NULL) {
-                g_debug ("Failed to get config \"wifi/submission-url\": %s",
-                         error->message);
-                g_clear_error (&error);
-                priv->wifi_submit_url = g_strdup (DEFAULT_WIFI_SUBMIT_URL);
-        }
+        load_string_value (config, "wifi", "submission-url",
+                           &priv->wifi_submit_url);
 
-        priv->wifi_submit_nick = g_key_file_get_string (priv->key_file,
-                                                        "wifi",
-                                                        "submission-nick",
-                                                        &error);
-        if (error != NULL) {
-                g_debug ("Failed to get config \"wifi/submission-nick\": %s",
-                         error->message);
-                g_error_free (error);
-                priv->wifi_submit_nick = g_strdup (DEFAULT_WIFI_SUBMIT_NICK);
+        if (load_string_value (config, "wifi", "submission-nick",
+                               &wifi_submit_nick)) {
+                /* Nickname must either be empty or 2 to 32 characters long */
+                size_t nick_length = strlen (wifi_submit_nick);
+                if (nick_length != 1 && nick_length <= 32) {
+                        g_clear_pointer (&priv->wifi_submit_nick, g_free);
+                        priv->wifi_submit_nick =
+                                g_steal_pointer (&wifi_submit_nick);
+                } else
+                        g_warning ("\"wifi/submission-nick\" must be empty "
+                                   "or between 2 to 32 characters long");
         }
 }
 
 static void
 load_3g_config (GClueConfig *config)
 {
-        config->priv->enable_3g_source =
-                load_enable_source_config (config, "3g");
+        load_boolean_value (config, "3g", "enable",
+                            &config->priv->enable_3g_source);
 }
 
 static void
 load_cdma_config (GClueConfig *config)
 {
-        config->priv->enable_cdma_source =
-                load_enable_source_config (config, "cdma");
+        load_boolean_value (config, "cdma", "enable",
+                            &config->priv->enable_cdma_source);
 }
 
 static void
 load_modem_gps_config (GClueConfig *config)
 {
-        config->priv->enable_modem_gps_source =
-                load_enable_source_config (config, "modem-gps");
+        load_boolean_value (config, "modem-gps", "enable",
+                            &config->priv->enable_modem_gps_source);
 }
 
 static void
 load_network_nmea_config (GClueConfig *config)
 {
-        GError *error = NULL;
-        config->priv->enable_nmea_source =
-                load_enable_source_config (config, "network-nmea");
-        if (!config->priv->enable_nmea_source)
-                return;
-        config->priv->nmea_socket = g_key_file_get_string (config->priv->key_file,
-                                                           "network-nmea",
-                                                           "nmea-socket",
-                                                           &error);
-        if (error != NULL) {
-                g_debug ("`nmea-socket` configuration not set.");
-                g_clear_error (&error);
-        }
+        load_boolean_value (config, "network-nmea", "enable",
+                            &config->priv->enable_nmea_source);
+        load_string_value (config, "network-nmea", "nmea-socket",
+                           &config->priv->nmea_socket);
 }
 
 static void
 load_compass_config (GClueConfig *config)
 {
-        config->priv->enable_compass =
-                load_enable_source_config (config, "compass");
+        load_boolean_value (config, "compass", "enable",
+                            &config->priv->enable_compass);
 }
 
 static void
-gclue_config_init (GClueConfig *config)
+load_static_source_config (GClueConfig *config)
 {
-        GError *error = NULL;
+        load_boolean_value (config, "static-source", "enable",
+                            &config->priv->enable_static_source);
+}
 
-        config->priv = gclue_config_get_instance_private(config);
-        config->priv->key_file = g_key_file_new ();
+static void
+load_config_file (GClueConfig *config, const char *path) {
+        g_autoptr(GError) error = NULL;
+
+        g_debug ("Loading config: %s", path);
         g_key_file_load_from_file (config->priv->key_file,
-                                   CONFIG_FILE_PATH,
+                                   path,
                                    0,
                                    &error);
         if (error != NULL) {
                 g_critical ("Failed to load configuration file '%s': %s",
-                            CONFIG_FILE_PATH, error->message);
-                g_error_free (error);
-
+                            path, error->message);
                 return;
         }
 
@@ -350,6 +402,201 @@ gclue_config_init (GClueConfig *config)
         load_modem_gps_config (config);
         load_network_nmea_config (config);
         load_compass_config (config);
+        load_static_source_config (config);
+}
+
+static void
+files_element_clear (void *element)
+{
+        gchar **file_name = element;
+        g_free (*file_name);
+}
+
+static gint
+sort_files (gconstpointer a, gconstpointer b)
+{
+        char *str_a = *(char **)a;
+        char *str_b = *(char **)b;
+
+        return g_strcmp0 (str_a, str_b);
+}
+
+static gboolean
+string_present (const gchar *str)
+{
+        return (str && str[0]);
+}
+
+static const gchar *
+string_or_none (const gchar *str)
+{
+        return (string_present (str) ? str : "none");
+}
+
+static const gchar *
+enabled_disabled (gboolean value)
+{
+        return (value ? "enabled" : "disabled");
+}
+
+static char *
+redact_api_key (char *url)
+{
+        char *match;
+
+        if (!string_present (url))
+                return NULL;
+
+        match = g_strrstr (url, "key=");
+        if (match && match > url && (*(match - 1) == '?' || *(match - 1) == '&')
+            && *(match + 4) != '\0') {
+                GString *s = g_string_new (url);
+                g_string_replace (s, match + 4, "<redacted>", 1);
+                return g_string_free (s, FALSE);
+        } else {
+                return g_strdup (url);
+        }
+}
+
+static void
+gclue_config_print (GClueConfig *config)
+{
+        GClueConfigPrivate *priv = config->priv;
+        GList *node;
+        AppConfig *app_config = NULL;
+        gsize i;
+
+        g_debug ("GeoClue configuration:");
+        if (priv->num_agents > 0) {
+                g_debug ("Allowed agents:");
+                for (i = 0; i < priv->num_agents; i++)
+                        g_debug ("\t%s", priv->agents[i]);
+        } else
+                g_debug ("Allowed agents: none");
+        g_debug ("Network NMEA source: %s",
+                 enabled_disabled (priv->enable_nmea_source));
+        g_debug ("\tNetwork NMEA socket: %s",
+                 string_or_none (priv->nmea_socket));
+        g_debug ("3G source: %s",
+                 enabled_disabled (priv->enable_3g_source));
+        g_debug ("CDMA source: %s",
+                 enabled_disabled (priv->enable_cdma_source));
+        g_debug ("Modem GPS source: %s",
+                 enabled_disabled (priv->enable_modem_gps_source));
+        g_debug ("WiFi source: %s",
+                 enabled_disabled (priv->enable_wifi_source));
+        {
+                g_autofree char *redacted_locate_url =
+                        redact_api_key (priv->wifi_url);
+                g_debug ("\tWiFi locate URL: %s",
+                         string_or_none (redacted_locate_url));
+        }
+        {
+                g_autofree char *redacted_submit_url =
+                        redact_api_key (priv->wifi_submit_url);
+                g_debug ("\tWiFi submit URL: %s",
+                         string_or_none (redacted_submit_url));
+        }
+        g_debug ("\tWiFi submit data: %s",
+                 enabled_disabled (priv->wifi_submit));
+        g_debug ("\tWiFi submission nickname: %s",
+                 string_or_none (priv->wifi_submit_nick));
+        g_debug ("Static source: %s",
+                 enabled_disabled (priv->enable_static_source));
+        g_debug ("Compass: %s",
+                 enabled_disabled (priv->enable_compass));
+        g_debug ("Application configs:");
+        for (node = priv->app_configs; node != NULL; node = node->next) {
+                app_config = (AppConfig *) node->data;
+                g_debug ("\tID: %s", app_config->id);
+                g_debug ("\t\tAllowed: %s", app_config->allowed? "yes": "no");
+                g_debug ("\t\tSystem: %s", app_config->system? "yes": "no");
+                if (app_config->num_users > 0) {
+                        g_debug ("\t\tUsers:");
+                        for (i = 0; i < app_config->num_users; i++)
+                                g_debug ("\t\t\t%d", app_config->users[i]);
+                } else
+                        g_debug ("\t\tUsers: all");
+        }
+}
+
+static void
+gclue_config_init (GClueConfig *config)
+{
+        GClueConfigPrivate *priv = gclue_config_get_instance_private (config);
+        g_autoptr(GDir) dir = NULL;
+        g_autoptr(GError) error = NULL;
+        g_autoptr(GArray) files = NULL;
+        char *name;
+        gsize i;
+
+        config->priv = priv;
+
+        /* Sources should be enabled by default */
+        priv->enable_nmea_source = TRUE;
+        priv->enable_3g_source = TRUE;
+        priv->enable_cdma_source = TRUE;
+        priv->enable_modem_gps_source = TRUE;
+        priv->enable_wifi_source = TRUE;
+        priv->enable_compass = TRUE;
+        priv->enable_static_source = TRUE;
+
+        /* Default strings */
+        priv->wifi_url = g_strdup (DEFAULT_WIFI_URL);
+        priv->wifi_submit_url = g_strdup (DEFAULT_WIFI_SUBMIT_URL);
+        priv->wifi_submit_nick = g_strdup (DEFAULT_WIFI_SUBMIT_NICK);
+
+        /* Load config file from default path, log all missing parameters */
+        priv->key_file = g_key_file_new ();
+        load_config_file (config, CONFIG_FILE_PATH);
+
+        /*
+         * Apply config overwrites from conf.d style config files,
+         * files are sorted alphabetically, example: '90-config.conf'
+         * will overwrite '50-config.conf'.
+         */
+        dir = g_dir_open (CONFIG_D_DIRECTORY, 0, &error);
+
+        if (error != NULL) {
+                if (!g_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_NOENT)) {
+                        g_warning ("Failed to open %s: %s",
+                                   CONFIG_D_DIRECTORY, error->message);
+                }
+                goto out;
+        }
+
+        files = g_array_new (FALSE, FALSE, sizeof(char *));
+        g_array_set_clear_func (files, files_element_clear);
+
+        while ((name = g_strdup (g_dir_read_name (dir)))) {
+                if (g_str_has_suffix (name, ".conf"))
+                        g_array_append_val (files, name);
+        }
+
+        g_array_sort (files, sort_files);
+
+        for (i = 0; i < files->len; i++) {
+                g_autofree char *path = NULL;
+
+                path = g_build_filename (CONFIG_D_DIRECTORY,
+                                         g_array_index (files, char *, i),
+                                         NULL);
+                load_config_file (config, path);
+        }
+out:
+        if (!string_present (priv->wifi_url) &&
+            (priv->enable_wifi_source || priv->enable_3g_source)) {
+                g_warning ("\"wifi/url\" is not set, "
+                           "disabling WiFi and 3G sources");
+                priv->enable_wifi_source = FALSE;
+                priv->enable_3g_source = FALSE;
+        }
+        if (!string_present (priv->wifi_submit_url) && priv->wifi_submit) {
+                g_warning ("\"wifi/submission-url\" is not set, "
+                           "disabling WiFi/3G submissions");
+                priv->wifi_submit = FALSE;
+        }
+        gclue_config_print (config);
 }
 
 GClueConfig *
@@ -479,7 +726,7 @@ void
 gclue_config_set_wifi_submit_nick (GClueConfig *config,
                                    const char  *nick)
 {
-
+        g_clear_pointer (&config->priv->wifi_submit_nick, g_free);
         config->priv->wifi_submit_nick = g_strdup (nick);
 }
 
@@ -528,9 +775,10 @@ gclue_config_get_enable_nmea_source (GClueConfig *config)
 
 void
 gclue_config_set_nmea_socket (GClueConfig *config,
-                                   const char  *nmea_socket)
+                              const char  *nmea_socket)
 {
-        config->priv->nmea_socket = g_strdup(nmea_socket);
+        g_clear_pointer (&config->priv->nmea_socket, g_free);
+        config->priv->nmea_socket = g_strdup (nmea_socket);
 }
 
 gboolean
@@ -538,3 +786,9 @@ gclue_config_get_enable_compass (GClueConfig *config)
 {
         return config->priv->enable_compass;
 }
+
+gboolean
+gclue_config_get_enable_static_source (GClueConfig *config)
+{
+        return config->priv->enable_static_source;
+}
diff --git a/src/gclue-config.h b/src/gclue-config.h
index 1231d56..db66f86 100644
--- a/src/gclue-config.h
+++ b/src/gclue-config.h
@@ -92,6 +92,8 @@ gboolean            gclue_config_get_enable_modem_gps_source
                                                         (GClueConfig     *config);
 gboolean            gclue_config_get_enable_nmea_source (GClueConfig     *config);
 gboolean            gclue_config_get_enable_compass     (GClueConfig     *config);
+gboolean            gclue_config_get_enable_static_source
+                                                        (GClueConfig *config);
 
 G_END_DECLS
 
diff --git a/src/gclue-location-source.c b/src/gclue-location-source.c
index 5d7347d..568b214 100644
--- a/src/gclue-location-source.c
+++ b/src/gclue-location-source.c
@@ -52,6 +52,7 @@ struct _GClueLocationSourcePrivate
 
         gboolean compute_movement;
         gboolean scramble_location;
+        gboolean priority_source;
 
 #if GCLUE_USE_COMPASS
         GClueCompass *compass;
@@ -74,6 +75,7 @@ enum
         PROP_AVAILABLE_ACCURACY_LEVEL,
         PROP_COMPUTE_MOVEMENT,
         PROP_SCRAMBLE_LOCATION,
+        PROP_PRIORITY_SOURCE,
         LAST_PROP
 };
 
@@ -156,6 +158,10 @@ gclue_location_source_get_property (GObject    *object,
                 g_value_set_boolean (value, source->priv->scramble_location);
                 break;
 
+        case PROP_PRIORITY_SOURCE:
+                g_value_set_boolean (value, source->priv->priority_source);
+                break;
+
         default:
                 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
         }
@@ -190,6 +196,10 @@ gclue_location_source_set_property (GObject      *object,
                 source->priv->scramble_location = g_value_get_boolean (value);
                 break;
 
+        case PROP_PRIORITY_SOURCE:
+                source->priv->priority_source = g_value_get_boolean (value);
+                break;
+
         default:
                 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
         }
@@ -281,6 +291,18 @@ gclue_location_source_class_init (GClueLocationSourceClass *klass)
         g_object_class_install_property (object_class,
                                          PROP_SCRAMBLE_LOCATION,
                                          gParamSpecs[PROP_SCRAMBLE_LOCATION]);
+
+        gParamSpecs[PROP_PRIORITY_SOURCE] =
+                g_param_spec_boolean ("priority-source",
+                                      "PrioritySource",
+                                      "Whether source is priority or not",
+                                      FALSE,
+                                      G_PARAM_READWRITE |
+                                      G_PARAM_CONSTRUCT_ONLY);
+        g_object_class_install_property (object_class,
+                                         PROP_PRIORITY_SOURCE,
+                                         gParamSpecs[PROP_PRIORITY_SOURCE]);
+
 }
 
 static void
@@ -289,6 +311,7 @@ gclue_location_source_init (GClueLocationSource *source)
         source->priv = gclue_location_source_get_instance_private (source);
         source->priv->compute_movement = TRUE;
         source->priv->time_threshold = gclue_min_uint_new ();
+        source->priv->priority_source = FALSE;
 }
 
 static GClueLocationSourceStartResult
@@ -419,27 +442,36 @@ gclue_location_source_set_location (GClueLocationSource *source,
         priv->location = gclue_location_duplicate (location);
 
         if (priv->scramble_location) {
-                gdouble latitude, distance, accuracy;
+                gdouble latitude, distance, accuracy, scramble_range;
 
                 latitude = gclue_location_get_latitude (priv->location);
                 accuracy = gclue_location_get_accuracy (priv->location);
 
+                scramble_range = GCLUE_LOCATION_ACCURACY_NEIGHBORHOOD;
+                if (accuracy >= scramble_range) {
+                        /* If the location source is already pretty inaccurate
+                         * do just a limited range scrambling to be sure.
+                         */
+                        scramble_range /= 3;
+                }
+
                 /* Randomization is needed to stop apps from calculationg the
                  * actual location.
                  */
-                distance = (gdouble) g_random_int_range (1, 3);
+                distance = g_random_double_range (0, scramble_range);
+                distance /= 1000;
 
                 if (g_random_boolean ())
                         latitude += distance * LATITUDE_IN_KM;
                 else
                         latitude -= distance * LATITUDE_IN_KM;
-                accuracy += 3000;
+                accuracy += scramble_range;
 
                 g_object_set (G_OBJECT (priv->location),
                               "latitude", latitude,
                               "accuracy", accuracy,
                               NULL);
-                g_debug ("location scrambled");
+                g_debug ("%s location scrambled", G_OBJECT_TYPE_NAME (source));
         }
 
         speed = gclue_location_get_speed (location);
@@ -489,6 +521,21 @@ gclue_location_source_get_active (GClueLocationSource *source)
         return (source->priv->active_counter > 0);
 }
 
+/**
+ * gclue_location_source_get_priority_source:
+ * @source: a #GClueLocationSource
+ *
+ * Returns: Returns: TRUE if source (e.g. GPS) has priority over other
+ * sources, FALSE otherwise.
+ **/
+gboolean
+gclue_location_source_get_priority_source (GClueLocationSource *source)
+{
+        g_return_val_if_fail (GCLUE_IS_LOCATION_SOURCE (source), FALSE);
+
+        return source->priv->priority_source;
+}
+
 /**
  * gclue_location_source_get_available_accuracy_level:
  * @source: a #GClueLocationSource
diff --git a/src/gclue-location-source.h b/src/gclue-location-source.h
index 620f601..2860c3a 100644
--- a/src/gclue-location-source.h
+++ b/src/gclue-location-source.h
@@ -71,6 +71,8 @@ struct _GClueLocationSourceClass
         GClueLocationSourceStopResult (*stop)  (GClueLocationSource *source);
 };
 
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (GClueLocationSource, g_object_unref)
+
 GType gclue_location_source_get_type (void) G_GNUC_CONST;
 
 void              gclue_location_source_start (GClueLocationSource *source);
@@ -82,6 +84,8 @@ void              gclue_location_source_set_location
                                                GClueLocation       *location);
 gboolean          gclue_location_source_get_active
                                               (GClueLocationSource *source);
+gboolean          gclue_location_source_get_priority_source
+                                              (GClueLocationSource *source);
 GClueAccuracyLevel
                   gclue_location_source_get_available_accuracy_level
                                               (GClueLocationSource *source);
diff --git a/src/gclue-location.c b/src/gclue-location.c
index b0e5e0d..da2a10a 100644
--- a/src/gclue-location.c
+++ b/src/gclue-location.c
@@ -29,9 +29,11 @@
 #include <string.h>
 #include <stdlib.h>
 
-#define TIME_DIFF_THRESHOLD 60000000 /* 60 seconds */
+#define TIME_DIFF_THRESHOLD (60 * G_USEC_PER_SEC) /* 60 seconds */
 #define EARTH_RADIUS_KM 6372.795
 #define KNOTS_IN_METERS_PER_SECOND 0.51444
+#define RMC_TIME_DIFF_THRESHOLD 5 /* 5 seconds */
+#define RMC_DEFAULT_ACCURACY 5    /* 5 meters */
 
 struct _GClueLocationPrivate {
         char   *description;
@@ -425,7 +427,7 @@ parse_coordinate_string (const char *coordinate,
                          const char *direction)
 {
         gdouble minutes, degrees, out;
-        gchar *degrees_str;
+        g_autofree gchar *degrees_str = NULL;
         gchar *dot_str;
         gint dot_offset;
 
@@ -450,7 +452,6 @@ parse_coordinate_string (const char *coordinate,
 
         degrees_str = g_strndup (coordinate, dot_offset - 2);
         degrees = g_ascii_strtod (degrees_str, NULL);
-        g_free (degrees_str);
 
         minutes = g_ascii_strtod (dot_str - 2, NULL);
 
@@ -480,65 +481,53 @@ parse_altitude_string (const char *altitude,
         return g_ascii_strtod (altitude, NULL);
 }
 
+/* Return a timestamp derived from the NMEA timestamp and system date as
+ * seconds since epoch.
+ * If system time cannot be retrieved, return 0.
+ * If timestamp parsing fails, return system time.
+ * If the parsed time is in the future when compared to the system time,
+ * return the parsed time yesterday.
+ */
 static gint64
 parse_nmea_timestamp (const char *nmea_ts)
 {
-        char parts[3][3];
-        int i, hours, minutes, seconds;
-        GDateTime *now, *ts = NULL;
-        guint64 ret;
+        g_autoptr(GDateTime) now = NULL;
+        g_autoptr(GDateTime) midnight = NULL;
+        g_autoptr(GDateTime) ts = NULL;
+        GTimeSpan timespan;
 
         now = g_date_time_new_now_utc ();
-        ret = g_date_time_to_unix (now);
-
-	if( now == NULL)
-                goto parse_error;
-
-        if (strlen (nmea_ts) < 6) {
-                if (strlen (nmea_ts) >= 1)
-                        /* Empty string just means no ts, so no warning */
-                        g_warning ("Failed to parse NMEA timestamp '%s'",
-                                   nmea_ts);
+        if (now == NULL) {
+                g_warning ("Failed to get the current UTC time");
+                return 0;
+        }
 
-                goto parse_error;
+        if (!nmea_ts || !*nmea_ts) {  /* Empty timestamp, no warning */
+                return g_date_time_to_unix (now);
         }
 
-        for (i = 0; i < 3; i++) {
-                memmove (parts[i], nmea_ts + (i * 2), 2);
-                parts[i][2] = '\0';
+        timespan = gclue_nmea_timestamp_to_timespan (nmea_ts);
+        if (timespan < 0) {
+                g_warning ("Failed to parse NMEA timestamp '%s'", nmea_ts);
+                return g_date_time_to_unix (now);
         }
-        hours = atoi (parts[0]);
-        minutes = atoi (parts[1]);
-        seconds = atoi (parts[2]);
-
-        ts = g_date_time_new_utc (g_date_time_get_year (now),
-                                  g_date_time_get_month (now),
-                                  g_date_time_get_day_of_month (now),
-                                  hours,
-                                  minutes,
-                                  seconds);
-        if (ts == NULL)
-                goto parse_error;
+
+        midnight = g_date_time_new_utc (g_date_time_get_year (now),
+                                        g_date_time_get_month (now),
+                                        g_date_time_get_day_of_month (now),
+                                        0,
+                                        0,
+                                        0);
+        ts = g_date_time_add (midnight, timespan);
 
         if (g_date_time_difference (ts, now) > TIME_DIFF_THRESHOLD) {
                 g_debug ("NMEA timestamp '%s' in future. Assuming yesterday's.",
                          nmea_ts);
-                g_date_time_unref (ts);
-
-                ts = g_date_time_new_utc (g_date_time_get_year (now),
-                                          g_date_time_get_month (now),
-                                          g_date_time_get_day_of_month (now) - 1,
-                                          hours,
-                                          minutes,
-                                          seconds);
+                g_clear_pointer (&ts, g_date_time_unref);
+                ts = g_date_time_add (midnight, timespan - G_TIME_SPAN_DAY);
         }
 
-        ret = g_date_time_to_unix (ts);
-        g_date_time_unref (ts);
-parse_error:
-        g_date_time_unref (now);
-
-        return ret;
+        return g_date_time_to_unix (ts);
 }
 
 /**
@@ -546,6 +535,7 @@ parse_error:
  * @latitude: a valid latitude
  * @longitude: a valid longitude
  * @accuracy: accuracy of location in meters
+ * @description: a description for the location
  *
  * Creates a new #GClueLocation object.
  *
@@ -554,12 +544,14 @@ parse_error:
 GClueLocation *
 gclue_location_new (gdouble latitude,
                     gdouble longitude,
-                    gdouble accuracy)
+                    gdouble accuracy,
+                    const char *description)
 {
         return g_object_new (GCLUE_TYPE_LOCATION,
                              "latitude", latitude,
                              "longitude", longitude,
                              "accuracy", accuracy,
+                             "description", description,
                              NULL);
 }
 
@@ -601,21 +593,23 @@ gclue_location_new_full (gdouble     latitude,
 }
 
 static GClueLocation *
-gclue_location_create_from_gga (const char *gga, GError **error)
+gclue_location_create_from_gga (const char *gga)
 {
-        GClueLocation *location = NULL;
+        GClueLocation *location;
         gdouble latitude, longitude, accuracy, altitude;
         gdouble hdop; /* Horizontal Dilution Of Precision */
         guint64 timestamp;
-        char **parts;
+        g_auto(GStrv) parts = NULL;
 
         parts = g_strsplit (gga, ",", -1);
         if (g_strv_length (parts) < 14) {
-                g_set_error_literal (error,
-                                     G_IO_ERROR,
-                                     G_IO_ERROR_INVALID_ARGUMENT,
-                                     "Invalid NMEA GGA sentence");
-                goto out;
+                g_warning ("Invalid NMEA GGA sentence.");
+                return NULL;
+        }
+
+        if (g_ascii_strtoll (parts[6], NULL, 10) == 0) {
+                /* No fix, ignore. */
+                return NULL;
         }
 
         /* For syntax of GGA sentences:
@@ -625,11 +619,8 @@ gclue_location_create_from_gga (const char *gga, GError **error)
         latitude = parse_coordinate_string (parts[2], parts[3]);
         longitude = parse_coordinate_string (parts[4], parts[5]);
         if (latitude == INVALID_COORDINATE || longitude == INVALID_COORDINATE) {
-                g_set_error_literal (error,
-                                     G_IO_ERROR,
-                                     G_IO_ERROR_INVALID_ARGUMENT,
-                                     "Invalid NMEA GGA sentence");
-                goto out;
+                g_warning ("Invalid coordinate on NMEA GGA sentence.");
+                return NULL;
         }
 
         altitude = parse_altitude_string (parts[9], parts[10]);
@@ -642,32 +633,42 @@ gclue_location_create_from_gga (const char *gga, GError **error)
                                  "longitude", longitude,
                                  "accuracy", accuracy,
                                  "timestamp", timestamp,
+                                 "description", "GPS GGA",
                                  NULL);
         if (altitude != GCLUE_LOCATION_ALTITUDE_UNKNOWN)
                 g_object_set (location, "altitude", altitude, NULL);
 
-out:
-        g_strfreev (parts);
         return location;
 }
 
 static GClueLocation *
 gclue_location_create_from_rmc (const char     *rmc,
-                                GClueLocation  *prev_location,
-                                GError        **error)
+                                GClueLocation  *prev_location)
 {
-        GClueLocation *location = NULL;
-        char **parts = g_strsplit (rmc, ",", -1);
+        GClueLocation *location;
+        g_auto(GStrv) parts = NULL;
+        gdouble accuracy;
+        gdouble altitude;
+
+        parts = g_strsplit (rmc, ",", -1);
+        if (g_strv_length (parts) < 12) {
+                g_warning ("Invalid NMEA RMC sentence.");
+                return NULL;
+        }
 
-        if (g_strv_length (parts) < 13)
-                goto error;
+        /* RMC sentence is invalid */
+        if (g_strcmp0 (parts[2], "A") != 0) {
+                return NULL;
+        }
 
         guint64 timestamp = parse_nmea_timestamp (parts[1]);
         gdouble lat = parse_coordinate_string (parts[3], parts[4]);
         gdouble lon = parse_coordinate_string (parts[5], parts[6]);
 
-        if (lat == INVALID_COORDINATE || lon == INVALID_COORDINATE)
-                goto error;
+        if (lat == INVALID_COORDINATE || lon == INVALID_COORDINATE) {
+                g_warning ("Invalid coordinate on NMEA RMC sentence.");
+                return NULL;
+        }
 
         gdouble speed = GCLUE_LOCATION_SPEED_UNKNOWN;
         if (parts[7][0] != '\0')
@@ -683,33 +684,34 @@ gclue_location_create_from_rmc (const char     *rmc,
                 heading = GCLUE_LOCATION_HEADING_UNKNOWN;
         }
 
+        accuracy = RMC_DEFAULT_ACCURACY;
+        altitude = GCLUE_LOCATION_ALTITUDE_UNKNOWN;
+        if (prev_location != NULL) {
+                guint64 prev_loc_timestamp;
+
+                prev_loc_timestamp = gclue_location_get_timestamp (prev_location);
+
+                /* Sentence is older then previous location, reject */
+                if (timestamp < prev_loc_timestamp)
+                        return NULL;
+
+                if (timestamp - prev_loc_timestamp < RMC_TIME_DIFF_THRESHOLD) {
+                        accuracy = gclue_location_get_accuracy (prev_location);
+                        altitude = gclue_location_get_altitude (prev_location);
+                }
+        }
+
         location = g_object_new (GCLUE_TYPE_LOCATION,
                                  "latitude", lat,
                                  "longitude", lon,
                                  "timestamp", timestamp,
                                  "speed", speed,
                                  "heading", heading,
+                                 "description", "GPS RMC",
+                                 "accuracy", accuracy,
+                                 "altitude", altitude,
                                  NULL);
 
-        if (prev_location != NULL) {
-                g_object_set (location,
-                              "accuracy",
-                              gclue_location_get_accuracy (prev_location),
-                              NULL);
-                g_object_set (location,
-                              "altitude",
-                              gclue_location_get_altitude (prev_location),
-                              NULL);
-        }
-
-        goto out;
-error:
-        g_set_error_literal (error,
-                             G_IO_ERROR,
-                             G_IO_ERROR_INVALID_ARGUMENT,
-                             "Invalid NMEA RMC sentence");
-out:
-        g_strfreev (parts);
         return location;
 }
 
@@ -717,7 +719,6 @@ out:
  * gclue_location_create_from_nmeas:
  * @nmea: A NULL terminated array NMEA sentence strings
  * @prev_location: Previous location provided from the location source
- * @error: Place-holder for errors.
  *
  * Creates a new #GClueLocation object by combining data from multiple NMEA
  * sentences.
@@ -728,8 +729,7 @@ out:
  **/
 GClueLocation *
 gclue_location_create_from_nmeas (const char     *nmeas[],
-                                  GClueLocation  *prev_location,
-                                  GError        **error)
+                                  GClueLocation  *prev_location)
 {
         GClueLocation *gga_loc = NULL;
         GClueLocation *rmc_loc = NULL;
@@ -737,10 +737,10 @@ gclue_location_create_from_nmeas (const char     *nmeas[],
 
         for (iter = nmeas; *iter != NULL; iter++) {
                 if (!gga_loc && gclue_nmea_type_is (*iter, "GGA"))
-                        gga_loc = gclue_location_create_from_gga (*iter, NULL);
+                        gga_loc = gclue_location_create_from_gga (*iter);
                 if (!rmc_loc && gclue_nmea_type_is (*iter, "RMC"))
                         rmc_loc = gclue_location_create_from_rmc
-                                (*iter, prev_location, NULL);
+                                (*iter, prev_location);
                 if (gga_loc && rmc_loc)
                     break;
         }
@@ -750,6 +750,7 @@ gclue_location_create_from_nmeas (const char     *nmeas[],
                         (gga_loc, gclue_location_get_speed(rmc_loc));
                 gclue_location_set_heading
                         (gga_loc, gclue_location_get_heading(rmc_loc));
+                g_object_set (gga_loc, "description", "GPS GGA+RMC", NULL);
                 g_object_unref (rmc_loc);
 
                 return gga_loc;
@@ -759,10 +760,7 @@ gclue_location_create_from_nmeas (const char     *nmeas[],
         if (rmc_loc)
                 return rmc_loc;
 
-        g_set_error_literal (error,
-                             G_IO_ERROR,
-                             G_IO_ERROR_INVALID_ARGUMENT,
-                             "Valid NMEA GGA or RMC sentence not found");
+        g_debug ("Valid NMEA GGA or RMC sentence not found");
         return NULL;
 }
 
@@ -770,7 +768,7 @@ gclue_location_create_from_nmeas (const char     *nmeas[],
  * gclue_location_duplicate:
  * @location: the #GClueLocation instance to duplicate.
  *
- * Creates a new copy of @location object.
+ * Creates a new copy of @location object (with the same timestamp).
  *
  * Returns: a new #GClueLocation object. Use g_object_unref() when done.
  **/
@@ -788,6 +786,32 @@ gclue_location_duplicate (GClueLocation *location)
                  "timestamp", location->priv->timestamp,
                  "speed", location->priv->speed,
                  "heading", location->priv->heading,
+                 "description", location->priv->description,
+                 NULL);
+}
+
+/**
+ * gclue_location_duplicate_fresh:
+ * @location: the #GClueLocation instance to duplicate.
+ *
+ * Creates a new copy of @location object with a refreshed timestamp.
+ *
+ * Returns: a new #GClueLocation object. Use g_object_unref() when done.
+ **/
+GClueLocation *
+gclue_location_duplicate_fresh (GClueLocation *location)
+{
+        g_return_val_if_fail (GCLUE_IS_LOCATION (location), NULL);
+
+        return g_object_new
+                (GCLUE_TYPE_LOCATION,
+                 "latitude", location->priv->latitude,
+                 "longitude", location->priv->longitude,
+                 "accuracy", location->priv->accuracy,
+                 "altitude", location->priv->altitude,
+                 "speed", location->priv->speed,
+                 "heading", location->priv->heading,
+                 "description", location->priv->description,
                  NULL);
 }
 
diff --git a/src/gclue-location.h b/src/gclue-location.h
index 0db57ac..40f2d6a 100644
--- a/src/gclue-location.h
+++ b/src/gclue-location.h
@@ -76,6 +76,13 @@ GType gclue_location_get_type (void);
  */
 #define GCLUE_LOCATION_ACCURACY_UNKNOWN -1
 
+/**
+ * GCLUE_LOCATION_ACCURACY_EXACT:
+ *
+ * Constant representing exact-level accuracy.
+ */
+#define GCLUE_LOCATION_ACCURACY_EXACT 50 /* 50 m */
+
 /**
  * GCLUE_LOCATION_ACCURACY_STREET:
  *
@@ -83,6 +90,13 @@ GType gclue_location_get_type (void);
  */
 #define GCLUE_LOCATION_ACCURACY_STREET 1000 /* 1 km */
 
+/**
+ * GCLUE_LOCATION_ACCURACY_NEIGHBORHOOD:
+ *
+ * Constant representing neighborhood-level accuracy.
+ */
+#define GCLUE_LOCATION_ACCURACY_NEIGHBORHOOD 3000 /* 3 km */
+
 /**
  * GCLUE_LOCATION_ACCURACY_CITY:
  *
@@ -94,6 +108,8 @@ GType gclue_location_get_type (void);
  * GCLUE_LOCATION_ACCURACY_REGION:
  *
  * Constant representing region-level accuracy.
+ *
+ * Currently unused.
  */
 #define GCLUE_LOCATION_ACCURACY_REGION 50000 /* 50 km */
 
@@ -108,6 +124,8 @@ GType gclue_location_get_type (void);
  * GCLUE_LOCATION_ACCURACY_CONTINENT:
  *
  * Constant representing continent-level accuracy.
+ *
+ * Currently unused.
  */
 #define GCLUE_LOCATION_ACCURACY_CONTINENT 3000000 /* 3000 km */
 
@@ -127,7 +145,8 @@ GType gclue_location_get_type (void);
 
 GClueLocation *gclue_location_new (gdouble latitude,
                                    gdouble longitude,
-                                   gdouble accuracy);
+                                   gdouble accuracy,
+                                   const char *description);
 
 GClueLocation *gclue_location_new_full
                                   (gdouble     latitude,
@@ -141,11 +160,12 @@ GClueLocation *gclue_location_new_full
 
 GClueLocation *gclue_location_create_from_nmeas
                                   (const char     *nmeas[],
-                                   GClueLocation  *prev_location,
-                                   GError        **error);
+                                   GClueLocation  *prev_location);
 
 GClueLocation *gclue_location_duplicate
                                   (GClueLocation *location);
+GClueLocation *gclue_location_duplicate_fresh
+                                  (GClueLocation *location);
 
 void gclue_location_set_description
                                   (GClueLocation *loc,
diff --git a/src/gclue-locator.c b/src/gclue-locator.c
index 150d829..d6ed560 100644
--- a/src/gclue-locator.c
+++ b/src/gclue-locator.c
@@ -2,6 +2,7 @@
 /* gclue-locator.c
  *
  * Copyright 2013 Red Hat, Inc.
+ * Copyright © 2022,2023 Oracle and/or its affiliates.
  *
  * Geoclue is free software; you can redistribute it and/or modify it under
  * the terms of the GNU General Public License as published by the Free
@@ -18,6 +19,7 @@
  * 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * Authors: Zeeshan Ali (Khattak) <zeeshanak@gnome.org>
+ *          Maciej S. Szmigiero <maciej.szmigiero@oracle.com>
  */
 
 #include "config.h"
@@ -26,6 +28,7 @@
 
 #include "gclue-locator.h"
 
+#include "gclue-static-source.h"
 #include "gclue-wifi.h"
 #include "gclue-config.h"
 
@@ -51,7 +54,7 @@
 
 static GClueLocationSourceStartResult
 gclue_locator_start (GClueLocationSource *source);
-static GClueLocationSourceStopResult 
+static GClueLocationSourceStopResult
 gclue_locator_stop (GClueLocationSource *source);
 
 struct _GClueLocatorPrivate
@@ -60,6 +63,8 @@ struct _GClueLocatorPrivate
         GList *active_sources;
 
         GClueAccuracyLevel accuracy_level;
+        gboolean priority_source_lock;
+        guint64 priority_source_lock_timestamp;
 };
 
 G_DEFINE_TYPE_WITH_CODE (GClueLocator,
@@ -78,18 +83,32 @@ static GParamSpec *gParamSpecs[LAST_PROP];
 
 #define MAX_SPEED        500       /* Meters per second */
 #define MAX_LOCATION_AGE (30 * 60) /* Seconds. */
+#define MAX_PRIORITY_SOURCE_AGE         30        /* Seconds. */
+#define PRIORITY_ACCURACY_THRESHOLD 20        /* Meters */
 
 static void
 set_location (GClueLocator  *locator,
-              GClueLocation *location)
+              GClueLocationSource *source)
 {
         GClueLocation *cur_location;
+        GClueLocation *location;
+        const char *src_name = NULL;
+        gboolean update_priority_source = FALSE;
+
+        location = gclue_location_source_get_location (source);
+        src_name = G_OBJECT_TYPE_NAME (source);
+
+        if (gclue_location_get_accuracy (location) ==
+            GCLUE_LOCATION_ACCURACY_UNKNOWN) {
+                /* If we do not know the accuracy, discard the update */
+                g_debug ("Discarding %s location with unknown accuracy",
+                         src_name);
+                return;
+        }
 
         cur_location = gclue_location_source_get_location
                         (GCLUE_LOCATION_SOURCE (locator));
 
-        g_debug ("New location available");
-
         if (cur_location != NULL) {
             guint64 cur_timestamp, new_timestamp;
             double dist, speed;
@@ -97,7 +116,8 @@ set_location (GClueLocator  *locator,
             cur_timestamp = gclue_location_get_timestamp (cur_location);
             new_timestamp = gclue_location_get_timestamp (location);
             if (new_timestamp < cur_timestamp) {
-                    g_debug ("New location older than current, ignoring.");
+                    g_debug ("New %s location older than current, ignoring.",
+                             src_name);
                     return;
             }
 
@@ -117,20 +137,46 @@ set_location (GClueLocator  *locator,
                 speed = G_MAXDOUBLE;
             }
 
-            if ((dist <= gclue_location_get_accuracy (location) ||
-                 speed > MAX_SPEED) &&
-                gclue_location_get_accuracy (location) >
-                gclue_location_get_accuracy (cur_location)) {
+            update_priority_source = gclue_location_source_get_priority_source (source) &&
+                                     (gclue_location_get_accuracy (location) < PRIORITY_ACCURACY_THRESHOLD);
+
+            if (update_priority_source) {
+                     if (!locator->priv->priority_source_lock)
+                              g_debug ("Enabling Priority Source Lock");
+                     locator->priv->priority_source_lock = TRUE;
+                     locator->priv->priority_source_lock_timestamp = new_timestamp;
+            } else if (locator->priv->priority_source_lock &&
+                       (new_timestamp - locator->priv->priority_source_lock_timestamp) >= MAX_PRIORITY_SOURCE_AGE) {
+                     g_debug ("Priority Source Lock no longer active");
+                     locator->priv->priority_source_lock = FALSE;
+            }
+
+            if (locator->priv->priority_source_lock &&
+                !gclue_location_source_get_priority_source (source)) {
+                     g_debug ("Priority Source Lock (age %u s) active, ignoring new %s location",
+                              (guint) (new_timestamp - locator->priv->priority_source_lock_timestamp),
+                              src_name);
+                     return;
+            }
+
+            if (update_priority_source) {
+                     /* A priority source is updating, let it though */
+                     g_debug ("Priority Source Lock Active");
+            } else if ((dist <= gclue_location_get_accuracy (location) ||
+                       speed > MAX_SPEED) &&
+                       gclue_location_get_accuracy (location) >
+                       gclue_location_get_accuracy (cur_location)) {
                     /* We only take the new location if either the previous one
                      * lies outside its accuracy circle and was reachable with
                      * a reasonable speed, OR it is more or as accurate as
                      * the previous one.
                      */
-                    g_debug ("Ignoring less accurate new location");
+                    g_debug ("Ignoring less accurate new %s location", src_name);
                     return;
             }
         }
 
+        g_debug ("New location available from %s", src_name);
         gclue_location_source_set_location (GCLUE_LOCATION_SOURCE (locator),
                                             location);
 }
@@ -180,10 +226,8 @@ on_location_changed (GObject    *gobject,
 {
         GClueLocator *locator = GCLUE_LOCATOR (user_data);
         GClueLocationSource *source = GCLUE_LOCATION_SOURCE (gobject);
-        GClueLocation *location;
 
-        location = gclue_location_source_get_location (source);
-        set_location (locator, location);
+        set_location (locator, source);
 }
 
 static gboolean
@@ -206,7 +250,7 @@ start_source (GClueLocator        *locator,
 
         location = gclue_location_source_get_location (src);
         if (gclue_location_source_get_active (src) && location != NULL)
-                set_location (locator, location);
+                set_location (locator, src);
 
         gclue_location_source_start (src);
 }
@@ -356,7 +400,7 @@ gclue_locator_constructed (GObject *object)
         GClueLocator *locator = GCLUE_LOCATOR (object);
         GClueLocationSource *submit_source = NULL;
         GClueConfig *gconfig = gclue_config_get_singleton ();
-        GClueWifi *wifi;
+        GClueWifi *wifi = NULL;
         GList *node;
         GClueMinUINT *threshold;
 
@@ -364,7 +408,7 @@ gclue_locator_constructed (GObject *object)
 
 #if GCLUE_USE_3G_SOURCE
         if (gclue_config_get_enable_3g_source (gconfig)) {
-                GClue3G *source = gclue_3g_get_singleton ();
+                GClue3G *source = gclue_3g_get_singleton (locator->priv->accuracy_level);
                 locator->priv->sources = g_list_append (locator->priv->sources,
                                                         source);
         }
@@ -376,18 +420,28 @@ gclue_locator_constructed (GObject *object)
                                                         cdma);
         }
 #endif
-        if (gclue_config_get_enable_wifi_source (gconfig))
+        if (gclue_config_get_enable_wifi_source (gconfig)) {
                 wifi = gclue_wifi_get_singleton (locator->priv->accuracy_level);
-        else
-                /* City-level accuracy will give us GeoIP-only source */
-                wifi = gclue_wifi_get_singleton (GCLUE_ACCURACY_LEVEL_CITY);
-        locator->priv->sources = g_list_append (locator->priv->sources, wifi);
+        } else {
+                if (gclue_config_get_enable_static_source (gconfig)) {
+                        g_debug ("Disabling GeoIP-only source since static source is enabled");
+                } else {
+                        /* City-level accuracy will give us GeoIP-only source */
+                        wifi = gclue_wifi_get_singleton (GCLUE_ACCURACY_LEVEL_CITY);
+                }
+        }
+        if (wifi) {
+                locator->priv->sources = g_list_append (locator->priv->sources,
+                                                        wifi);
+        }
 #if GCLUE_USE_MODEM_GPS_SOURCE
         if (gclue_config_get_enable_modem_gps_source (gconfig)) {
                 GClueModemGPS *gps = gclue_modem_gps_get_singleton ();
                 locator->priv->sources = g_list_append (locator->priv->sources,
                                                         gps);
-                submit_source = GCLUE_LOCATION_SOURCE (gps);
+                if (!submit_source) {
+                        submit_source = GCLUE_LOCATION_SOURCE (gps);
+                }
         }
 #endif
 #if GCLUE_USE_NMEA_SOURCE
@@ -395,9 +449,22 @@ gclue_locator_constructed (GObject *object)
                 GClueNMEASource *nmea = gclue_nmea_source_get_singleton ();
                 locator->priv->sources = g_list_append (locator->priv->sources,
                                                         nmea);
+                if (!submit_source) {
+                        submit_source = GCLUE_LOCATION_SOURCE (nmea);
+                }
+
         }
 #endif
 
+        if (gclue_config_get_enable_static_source (gconfig)) {
+                GClueStaticSource *static_source;
+
+                static_source = gclue_static_source_get_singleton
+                        (locator->priv->accuracy_level);
+                locator->priv->sources = g_list_append (locator->priv->sources,
+                                                        static_source);
+        }
+
         for (node = locator->priv->sources; node != NULL; node = node->next) {
                 g_signal_connect (G_OBJECT (node->data),
                                   "notify::available-accuracy-level",
@@ -449,6 +516,8 @@ static void
 gclue_locator_init (GClueLocator *locator)
 {
         locator->priv = gclue_locator_get_instance_private (locator);
+        locator->priv->priority_source_lock = FALSE;
+        locator->priv->priority_source_lock_timestamp = 0;
 }
 
 static GClueLocationSourceStartResult
diff --git a/src/gclue-main.c b/src/gclue-main.c
index d14cadc..9b32170 100644
--- a/src/gclue-main.c
+++ b/src/gclue-main.c
@@ -66,7 +66,7 @@ static GOptionEntry entries[] =
           0,
           G_OPTION_ARG_STRING,
           &submit_nick,
-          N_("Nickname to submit network data under (2-32 characters)"),
+          N_("Nickname to submit network data under (empty or 2 to 32 characters)"),
           "NICK" },
         { "nmea-socket",
           'u',
@@ -123,12 +123,11 @@ on_bus_acquired (GDBusConnection *connection,
                  const gchar     *name,
                  gpointer         user_data)
 {
-        GError *error = NULL;
+        g_autoptr(GError) error = NULL;
 
         manager = gclue_service_manager_new (connection, &error);
         if (manager == NULL) {
                 g_critical ("Failed to register server: %s", error->message);
-                g_error_free (error);
 
                 exit (-2);
         }
@@ -159,7 +158,7 @@ int
 main (int argc, char **argv)
 {
         guint owner_id;
-        GError *error = NULL;
+        g_autoptr(GError) error = NULL;
         GOptionContext *context;
         GClueConfig *config;
 
diff --git a/src/gclue-modem-gps.c b/src/gclue-modem-gps.c
index ce4967a..5b348d2 100644
--- a/src/gclue-modem-gps.c
+++ b/src/gclue-modem-gps.c
@@ -80,14 +80,15 @@ on_gps_enabled (GObject      *source_object,
                 GAsyncResult *result,
                 gpointer      user_data)
 {
-        GClueModemGPS *source = GCLUE_MODEM_GPS (user_data);
-        GError *error = NULL;
+        g_autoptr(GError) error = NULL;
 
-        if (!gclue_modem_enable_gps_finish (source->priv->modem,
+        if (!gclue_modem_enable_gps_finish (GCLUE_MODEM (source_object),
                                             result,
                                             &error)) {
-                g_warning ("Failed to enable GPS: %s", error->message);
-                g_error_free (error);
+                if (error && !g_error_matches (error, G_IO_ERROR,
+                                               G_IO_ERROR_CANCELLED)) {
+                        g_warning ("Failed to enable GPS: %s", error->message);
+                }
         }
 }
 
@@ -168,10 +169,10 @@ gclue_modem_gps_init (GClueModemGPS *source)
                                           source);
         threshold = gclue_location_source_get_time_threshold
                         (GCLUE_LOCATION_SOURCE (source));
-        g_signal_connect (threshold,
-                          "notify::value",
-                          G_CALLBACK (on_time_threshold_changed),
-                          source);
+        g_signal_connect_object (threshold,
+                                 "notify::value",
+                                 G_CALLBACK (on_time_threshold_changed),
+                                 source, 0);
 }
 
 static void
@@ -197,7 +198,9 @@ gclue_modem_gps_get_singleton (void)
         static GClueModemGPS *source = NULL;
 
         if (source == NULL) {
-                source = g_object_new (GCLUE_TYPE_MODEM_GPS, NULL);
+                source = g_object_new (GCLUE_TYPE_MODEM_GPS,
+                                       "priority-source", TRUE,
+                                       NULL);
                 g_object_weak_ref (G_OBJECT (source),
                                    on_modem_gps_destroyed,
                                    &source);
@@ -215,22 +218,13 @@ on_fix_gps (GClueModem *modem,
         GClueLocationSource *source = GCLUE_LOCATION_SOURCE (user_data);
         GClueLocation *prev_location;
         g_autoptr(GClueLocation) location = NULL;
-        GError *error = NULL;
 
         prev_location = gclue_location_source_get_location (source);
-        location = gclue_location_create_from_nmeas (nmeas,
-                                                     prev_location,
-                                                     &error);
+        location = gclue_location_create_from_nmeas (nmeas, prev_location);
 
-        if (error != NULL) {
-            g_warning ("Error: %s", error->message);
-            g_clear_error (&error);
-
-            return;
+        if (location) {
+                gclue_location_source_set_location (source, location);
         }
-
-        gclue_location_source_set_location (source,
-                                            location);
 }
 
 static GClueLocationSourceStartResult
@@ -268,7 +262,7 @@ gclue_modem_gps_stop (GClueLocationSource *source)
 {
         GClueModemGPSPrivate *priv = GCLUE_MODEM_GPS (source)->priv;
         GClueLocationSourceClass *base_class;
-        GError *error = NULL;
+        g_autoptr(GError) error = NULL;
         GClueLocationSourceStopResult base_result;
 
         g_return_val_if_fail (GCLUE_IS_LOCATION_SOURCE (source), FALSE);
@@ -288,7 +282,6 @@ gclue_modem_gps_stop (GClueLocationSource *source)
                                               &error)) {
                         g_warning ("Failed to disable GPS: %s",
                                    error->message);
-                        g_error_free (error);
                 }
 
         return base_result;
diff --git a/src/gclue-modem-manager.c b/src/gclue-modem-manager.c
index 9a8cf57..e6ee823 100644
--- a/src/gclue-modem-manager.c
+++ b/src/gclue-modem-manager.c
@@ -41,15 +41,20 @@ gclue_modem_interface_init (GClueModemInterface *iface);
 
 struct _GClueModemManagerPrivate {
         MMManager *manager;
+
+        GHashTable *modems_not_enabled;
+
         MMObject *mm_object;
         MMModem *modem;
         MMModemLocation *modem_location;
         MMLocation3gpp *location_3gpp;
+        gboolean location_3gpp_ignore_previous;
         MMLocationGpsNmea *location_nmea;
 
         GCancellable *cancellable;
 
         MMModemLocationSource caps; /* Caps we set or are going to set */
+        GClueTowerTec tec;
 
         guint time_threshold;
 };
@@ -145,6 +150,7 @@ gclue_modem_manager_finalize (GObject *gmodem)
         g_clear_object (&priv->mm_object);
         g_clear_object (&priv->modem);
         g_clear_object (&priv->modem_location);
+        g_clear_pointer (&priv->modems_not_enabled, g_hash_table_unref);
 }
 
 static void
@@ -286,7 +292,8 @@ static gboolean
 is_location_3gpp_same (GClueModemManager *manager,
                        const gchar       *new_opc,
                        gulong             new_lac,
-                       gulong             new_cell_id)
+                       gulong             new_cell_id,
+                       GClueTowerTec      new_tec)
 {
         GClueModemManagerPrivate *priv = manager->priv;
         const gchar *opc;
@@ -295,7 +302,7 @@ is_location_3gpp_same (GClueModemManager *manager,
         gchar opc_buf[GCLUE_3G_TOWER_OPERATOR_CODE_STR_LEN + 1];
 #endif
 
-        if (priv->location_3gpp == NULL)
+        if (priv->location_3gpp == NULL || priv->location_3gpp_ignore_previous)
                 return FALSE;
 
 #if MM_CHECK_VERSION(1, 18, 0)
@@ -306,11 +313,10 @@ is_location_3gpp_same (GClueModemManager *manager,
 #endif
         lac = mm_location_3gpp_get_location_area_code (priv->location_3gpp);
 
-        // Most likely this is an LTE connection and with the mozilla
-        // services they use the tracking area code in place of the
-        // location area code in this case.
+        // Use the tracking area code in place of the
+        // location area code for LTE.
         // https://ichnaea.readthedocs.io/en/latest/api/geolocate.html#cell-tower-fields
-        if (lac == 0x0 || lac == 0xFFFE) {
+        if (priv->tec == GCLUE_TOWER_TEC_4G) {
                 lac = mm_location_3gpp_get_tracking_area_code(priv->location_3gpp);
         }
 
@@ -318,7 +324,20 @@ is_location_3gpp_same (GClueModemManager *manager,
 
         return (g_strcmp0 (opc, new_opc) == 0 &&
                 lac == new_lac &&
-                cell_id == new_cell_id);
+                cell_id == new_cell_id &&
+                priv->tec == new_tec);
+}
+
+static void clear_3gpp_location (GClueModemManager *manager)
+{
+        GClueModemManagerPrivate *priv = manager->priv;
+
+        if (!priv->location_3gpp && !priv->location_3gpp_ignore_previous) {
+                return;
+        }
+
+        g_clear_object (&priv->location_3gpp);
+        g_signal_emit (manager, signals[FIX_3G], 0, NULL, 0, 0, GCLUE_TOWER_TEC_NO_FIX);
 }
 
 static void
@@ -326,30 +345,40 @@ on_get_3gpp_ready (GObject      *source_object,
                    GAsyncResult *res,
                    gpointer      user_data)
 {
-        GClueModemManager *manager = GCLUE_MODEM_MANAGER (user_data);
-        GClueModemManagerPrivate *priv = manager->priv;
+        GClueModemManager *manager;
+        GClueModemManagerPrivate *priv;
         MMModemLocation *modem_location = MM_MODEM_LOCATION (source_object);
+        MMModemAccessTechnology modem_access_tec;
         g_autoptr(MMLocation3gpp) location_3gpp = NULL;
-        GError *error = NULL;
         const gchar *opc;
         gulong lac, cell_id;
-        GClueTowerTec tec = GCLUE_TOWER_TEC_3G;
+        GClueTowerTec tec;
 #if !MM_CHECK_VERSION(1, 18, 0)
+        g_autoptr(GError) error = NULL;
         gchar opc_buf[GCLUE_3G_TOWER_OPERATOR_CODE_STR_LEN + 1];
-#endif
 
         location_3gpp = mm_modem_location_get_3gpp_finish (modem_location,
                                                            res,
                                                            &error);
         if (error != NULL) {
-                g_warning ("Failed to get location from 3GPP: %s",
-                           error->message);
-                g_error_free (error);
+                if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
+                        g_warning ("Failed to get location from 3GPP: %s",
+                                   error->message);
+                }
+
                 return;
         }
+#else
+        location_3gpp = mm_modem_location_get_signaled_3gpp (modem_location);
+#endif
+
+        manager = GCLUE_MODEM_MANAGER (user_data);
+        priv = manager->priv;
 
         if (location_3gpp == NULL) {
                 g_debug ("No 3GPP");
+                clear_3gpp_location (manager);
+                priv->location_3gpp_ignore_previous = FALSE;
                 return;
         }
 
@@ -364,46 +393,80 @@ on_get_3gpp_ready (GObject      *source_object,
 
         lac = mm_location_3gpp_get_location_area_code (location_3gpp);
 
-        // Most likely this is an LTE connection and with the mozilla
-        // services they use the tracking area code in place of the
-        // location area code in this case.
-        // https://ichnaea.readthedocs.io/en/latest/api/geolocate.html#cell-tower-fields
-        if (lac == 0x0 || lac == 0xFFFE) {
+        cell_id = mm_location_3gpp_get_cell_id (location_3gpp);
+
+        modem_access_tec = mm_modem_get_access_technologies(priv->modem);
+
+        if (modem_access_tec == MM_MODEM_ACCESS_TECHNOLOGY_GSM ||
+            modem_access_tec == MM_MODEM_ACCESS_TECHNOLOGY_GPRS ||
+            modem_access_tec == MM_MODEM_ACCESS_TECHNOLOGY_EDGE) {
+                tec = GCLUE_TOWER_TEC_2G;
+        } else if (modem_access_tec == MM_MODEM_ACCESS_TECHNOLOGY_UMTS ||
+                   modem_access_tec == MM_MODEM_ACCESS_TECHNOLOGY_HSDPA ||
+                   modem_access_tec == MM_MODEM_ACCESS_TECHNOLOGY_HSUPA ||
+                   modem_access_tec == MM_MODEM_ACCESS_TECHNOLOGY_HSPA ||
+                   modem_access_tec == MM_MODEM_ACCESS_TECHNOLOGY_HSPA_PLUS) {
+                tec = GCLUE_TOWER_TEC_3G;
+        } else if (modem_access_tec == MM_MODEM_ACCESS_TECHNOLOGY_LTE) {
                 lac = mm_location_3gpp_get_tracking_area_code(location_3gpp);
                 tec = GCLUE_TOWER_TEC_4G;
+        } else {
+                tec = GCLUE_TOWER_TEC_UNKNOWN;
         }
 
-        cell_id = mm_location_3gpp_get_cell_id (location_3gpp);
-
-        if (is_location_3gpp_same (manager, opc, lac, cell_id)) {
+        if (is_location_3gpp_same (manager, opc, lac, cell_id, tec)) {
                 g_debug ("New 3GPP location is same as last one");
                 return;
         }
         g_clear_object (&priv->location_3gpp);
         priv->location_3gpp = g_steal_pointer (&location_3gpp);
+        priv->location_3gpp_ignore_previous = FALSE;
+        priv->tec = tec;
 
         g_signal_emit (manager, signals[FIX_3G], 0, opc, lac, cell_id, tec);
 }
 
+static void
+on_location_changed_get_3gpp (GObject *modem_object,
+                              GClueModemManager *manager)
+{
+#if MM_CHECK_VERSION(1, 18, 0)
+        on_get_3gpp_ready(modem_object, NULL, manager);
+#else
+        mm_modem_location_get_3gpp (MM_MODEM_LOCATION (modem_object),
+                                    manager->priv->cancellable,
+                                    on_get_3gpp_ready,
+                                    manager);
+#endif
+}
+
 static void
 on_get_cdma_ready (GObject      *source_object,
                    GAsyncResult *res,
                    gpointer      user_data)
 {
-        GClueModemManager *manager = GCLUE_MODEM_MANAGER (user_data);
+        GClueModemManager *manager;
         MMModemLocation *modem_location = MM_MODEM_LOCATION (source_object);
         g_autoptr(MMLocationCdmaBs) location_cdma = NULL;
-        GError *error = NULL;
+#if !MM_CHECK_VERSION(1, 18, 0)
+        g_autoptr(GError) error = NULL;
 
         location_cdma = mm_modem_location_get_cdma_bs_finish (modem_location,
                                                               res,
                                                               &error);
         if (error != NULL) {
-                g_warning ("Failed to get location from 3GPP: %s",
-                           error->message);
-                g_error_free (error);
+                if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
+                        g_warning ("Failed to get location from CDMA: %s",
+                                   error->message);
+                }
+
                 return;
         }
+#else
+        location_cdma = mm_modem_location_get_signaled_cdma_bs (modem_location);
+#endif
+
+        manager = GCLUE_MODEM_MANAGER (user_data);
 
         if (location_cdma == NULL) {
                 g_debug ("No CDMA");
@@ -417,6 +480,20 @@ on_get_cdma_ready (GObject      *source_object,
                        mm_location_cdma_bs_get_longitude (location_cdma));
 }
 
+static void
+on_location_changed_get_cdma (GObject *modem_object,
+                              GClueModemManager *manager)
+{
+#if MM_CHECK_VERSION(1, 18, 0)
+        on_get_cdma_ready(modem_object, NULL, manager);
+#else
+        mm_modem_location_get_cdma_bs (MM_MODEM_LOCATION (modem_object),
+                                       manager->priv->cancellable,
+                                       on_get_cdma_ready,
+                                       manager);
+#endif
+}
+
 static gboolean
 is_location_gga_same (GClueModemManager *manager,
                        const char       *new_gga)
@@ -436,24 +513,33 @@ on_get_gps_nmea_ready (GObject      *source_object,
                        GAsyncResult *res,
                        gpointer      user_data)
 {
-        GClueModemManager *manager = GCLUE_MODEM_MANAGER (user_data);
-        GClueModemManagerPrivate *priv = manager->priv;
+        GClueModemManager *manager;
+        GClueModemManagerPrivate *priv;
         MMModemLocation *modem_location = MM_MODEM_LOCATION (source_object);
         g_autoptr(MMLocationGpsNmea) location_nmea = NULL;
         static const gchar *sentences[3];
         const gchar *gga, *rmc;
         gint i = 0;
-        GError *error = NULL;
+#if !MM_CHECK_VERSION(1, 18, 0)
+        g_autoptr(GError) error = NULL;
 
         location_nmea = mm_modem_location_get_gps_nmea_finish (modem_location,
                                                                res,
                                                                &error);
         if (error != NULL) {
-                g_warning ("Failed to get location from NMEA information: %s",
-                           error->message);
-                g_error_free (error);
+                if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
+                        g_warning ("Failed to get location from NMEA information: %s",
+                                   error->message);
+                }
+
                 return;
         }
+#else
+        location_nmea = mm_modem_location_get_signaled_gps_nmea (modem_location);
+#endif
+
+        manager = GCLUE_MODEM_MANAGER (user_data);
+        priv = manager->priv;
 
         if (location_nmea == NULL) {
                 g_debug ("No NMEA");
@@ -485,29 +571,33 @@ on_get_gps_nmea_ready (GObject      *source_object,
         priv->location_nmea = g_steal_pointer (&location_nmea);
 }
 
+static void
+on_location_changed_get_gps_nmea (GObject    *modem_object,
+                                  GClueModemManager *manager)
+{
+#if MM_CHECK_VERSION(1, 18, 0)
+        on_get_gps_nmea_ready(modem_object, NULL, manager);
+#else
+        mm_modem_location_get_gps_nmea (MM_MODEM_LOCATION (modem_object),
+                                        manager->priv->cancellable,
+                                        on_get_gps_nmea_ready,
+                                        manager);
+#endif
+}
+
 static void
 on_location_changed (GObject    *modem_object,
                      GParamSpec *pspec,
                      gpointer    user_data)
 {
-        MMModemLocation *modem_location = MM_MODEM_LOCATION (modem_object);
         GClueModemManager *manager = GCLUE_MODEM_MANAGER (user_data);
 
         if ((manager->priv->caps & MM_MODEM_LOCATION_SOURCE_3GPP_LAC_CI) != 0)
-                mm_modem_location_get_3gpp (modem_location,
-                                            manager->priv->cancellable,
-                                            on_get_3gpp_ready,
-                                            manager);
+                on_location_changed_get_3gpp (modem_object, manager);
         if ((manager->priv->caps & MM_MODEM_LOCATION_SOURCE_CDMA_BS) != 0)
-                mm_modem_location_get_cdma_bs (modem_location,
-                                               manager->priv->cancellable,
-                                               on_get_cdma_ready,
-                                               manager);
+                on_location_changed_get_cdma (modem_object, manager);
         if ((manager->priv->caps & MM_MODEM_LOCATION_SOURCE_GPS_NMEA) != 0)
-                mm_modem_location_get_gps_nmea (modem_location,
-                                                manager->priv->cancellable,
-                                                on_get_gps_nmea_ready,
-                                                manager);
+                on_location_changed_get_gps_nmea (modem_object, manager);
 }
 
 static void
@@ -516,8 +606,9 @@ on_modem_location_setup (GObject      *modem_object,
                          gpointer      user_data)
 {
         GTask *task = G_TASK (user_data);
-        GClueModemManager *manager;
-        GClueModemManagerPrivate *priv;
+        GClueModemManager *manager = GCLUE_MODEM_MANAGER
+                (g_task_get_source_object (task));
+        GClueModemManagerPrivate *priv = manager->priv;
         GError *error = NULL;
 
         if (!mm_modem_location_setup_finish (MM_MODEM_LOCATION (modem_object),
@@ -527,10 +618,11 @@ on_modem_location_setup (GObject      *modem_object,
 
                 goto out;
         }
-        manager = GCLUE_MODEM_MANAGER (g_task_get_source_object (task));
-        priv = manager->priv;
+
         g_debug ("Modem '%s' setup.", mm_object_get_path (priv->mm_object));
 
+        /* Make sure that we actually emit that signal */
+        priv->location_3gpp_ignore_previous = TRUE;
         on_location_changed (modem_object, NULL, manager);
 
         g_task_return_boolean (task, TRUE);
@@ -551,8 +643,6 @@ enable_caps (GClueModemManager    *manager,
         priv->caps |= caps;
         task = g_task_new (manager, cancellable, callback, user_data);
 
-        priv = GCLUE_MODEM_MANAGER (g_task_get_source_object (task))->priv;
-
         caps = mm_modem_location_get_enabled (priv->modem_location) | priv->caps;
         mm_modem_location_setup (priv->modem_location,
                                  caps,
@@ -611,9 +701,149 @@ modem_has_caps (GClueModemManager    *manager,
 }
 
 static void
-on_mm_object_added (GDBusObjectManager *object_manager,
-                    GDBusObject        *object,
-                    gpointer            user_data);
+on_enable_agps_ready (GObject      *source,
+                      GAsyncResult *result,
+                      gpointer      user_data)
+{
+        GClueModemManager *manager;
+        g_autoptr(GError) error = NULL;
+
+        g_return_if_fail (GCLUE_IS_MODEM_MANAGER (source));
+        manager = GCLUE_MODEM_MANAGER (source);
+
+        if (!enable_caps_finish (manager, result, &error)) {
+                g_warning ("Failed to enable assisted GPS: %s", error->message);
+                /* Clear AGPS caps so that subsequent calls to enable_caps do not fail */
+                manager->priv->caps &= ~(MM_MODEM_LOCATION_SOURCE_AGPS_MSB | MM_MODEM_LOCATION_SOURCE_AGPS_MSA);
+        }
+}
+
+static void
+enable_agps (GClueModemManager *manager)
+{
+        MMModemLocationSource assistance_caps = MM_MODEM_LOCATION_SOURCE_NONE;
+
+        g_return_if_fail (
+                gclue_modem_manager_get_is_gps_available (GCLUE_MODEM (manager)));
+
+        if (manager->priv->modem_location
+            && mm_modem_location_get_supl_server (manager->priv->modem_location) != NULL) {
+                MMModemLocationSource caps;
+
+                caps = mm_modem_location_get_capabilities (manager->priv->modem_location);
+                /* Prefer MSB assistance */
+                if (caps & MM_MODEM_LOCATION_SOURCE_AGPS_MSB) {
+                        assistance_caps |= MM_MODEM_LOCATION_SOURCE_AGPS_MSB;
+                        g_debug ("Enabing MSB assisted GPS");
+                } else if (caps & MM_MODEM_LOCATION_SOURCE_AGPS_MSA) {
+                        assistance_caps |= MM_MODEM_LOCATION_SOURCE_AGPS_MSA;
+                        g_debug ("Enabling MSA assisted GPS");
+                }
+        }
+        if (assistance_caps == MM_MODEM_LOCATION_SOURCE_NONE) {
+                g_debug ("Assisted GPS not available");
+                return;
+        }
+
+        enable_caps (manager,
+                     assistance_caps,
+                     manager->priv->cancellable,
+                     on_enable_agps_ready,
+                     manager);
+}
+
+static void
+disconnect_modem_location (GClueModemManager *manager)
+{
+        GClueModemManagerPrivate *priv = manager->priv;
+
+        if (!priv->modem_location) {
+                return;
+        }
+
+        g_signal_handlers_disconnect_by_func (G_OBJECT (priv->modem_location),
+                                              G_CALLBACK (on_location_changed),
+                                              manager);
+
+        g_clear_object (&priv->modem_location);
+}
+
+static void
+on_gps_refresh_rate_set (GObject      *source_object,
+                         GAsyncResult *res,
+                         gpointer      user_data)
+{
+        g_autoptr(GError) error = NULL;
+
+        mm_modem_location_set_gps_refresh_rate_finish
+                (MM_MODEM_LOCATION (source_object), res, &error);
+        if (error && !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
+                g_warning ("Failed to set GPS refresh rate: %s",
+                           error->message);
+                /* TODO: try selecting better modem if GPS is unsupported? */
+        }
+}
+
+static gboolean
+try_modem_location (GClueModemManager *manager,
+                    MMObject *mm_object)
+{
+        const char *path = mm_object_get_path (mm_object);
+        g_autoptr(MMModemLocation) modem_location = NULL;
+
+        modem_location = mm_object_get_modem_location (mm_object);
+        if (modem_location == NULL) {
+                g_debug ("Modem '%s' does not have location capabilities", path);
+                return FALSE;
+        }
+
+        /* TODO: check that modem actually has some usable capabilities, like GNSS */
+        g_debug ("Modem '%s' has location capabilities", path);
+
+        g_assert (!manager->priv->modem_location);
+        manager->priv->modem_location = g_object_ref (modem_location);
+
+        mm_modem_location_set_gps_refresh_rate (manager->priv->modem_location,
+                                                manager->priv->time_threshold,
+                                                manager->priv->cancellable,
+                                                on_gps_refresh_rate_set,
+                                                NULL);
+
+        g_signal_connect (G_OBJECT (manager->priv->modem_location),
+                          "notify::location",
+                          G_CALLBACK (on_location_changed),
+                          manager);
+
+        return TRUE;
+}
+
+static void
+try_modem (GClueModemManager *manager,
+           MMObject *mm_object,
+           MMModem *mm_modem,
+           gboolean modem_is_enabled)
+{
+        const char *path = mm_object_get_path (mm_object);
+
+        if (!try_modem_location (manager,
+                                 mm_object)) {
+                return;
+        }
+
+        manager->priv->mm_object = g_object_ref (mm_object);
+        manager->priv->modem = g_object_ref (mm_modem);
+
+        /* Has to be done after setting up manager->priv objects */
+        g_object_notify_by_pspec (G_OBJECT (manager), gParamSpecs[PROP_IS_GPS_AVAILABLE]);
+
+        if (modem_is_enabled) {
+                g_object_notify_by_pspec (G_OBJECT (manager), gParamSpecs[PROP_IS_3G_AVAILABLE]);
+                g_object_notify_by_pspec (G_OBJECT (manager), gParamSpecs[PROP_IS_CDMA_AVAILABLE]);
+                enable_agps (manager);
+        } else {
+                g_debug ("3G or CDMA are not available on non-enabled modem '%s'", path);
+        }
+}
 
 static void
 on_mm_modem_state_notify (GObject    *gobject,
@@ -625,46 +855,43 @@ on_mm_modem_state_notify (GObject    *gobject,
         GClueModemManagerPrivate *priv = manager->priv;
         GDBusObjectManager *obj_manager = G_DBUS_OBJECT_MANAGER (priv->manager);
         const char *path = mm_modem_get_path (mm_modem);
-        GDBusObject *object;
-
-        if (priv->mm_object != NULL) {
-                // In the meantime another modem with location caps was found.
-                g_signal_handlers_disconnect_by_func (mm_modem,
-                                                      on_mm_modem_state_notify,
-                                                      user_data);
-                g_object_unref (gobject);
-
-                return;
-        }
+        g_autoptr(MMObject) mm_object = NULL;
 
         if (mm_modem_get_state (mm_modem) < MM_MODEM_STATE_ENABLED)
                 return;
 
-        g_debug ("Modem '%s' now enabled", path);
-
         g_signal_handlers_disconnect_by_func (mm_modem,
                                               on_mm_modem_state_notify,
                                               user_data);
 
-        object = g_dbus_object_manager_get_object (obj_manager, path);
-        on_mm_object_added (obj_manager, object, user_data);
-        g_object_unref (mm_modem);
-}
+        if (priv->modem != NULL && priv->modem != mm_modem) {
+                g_debug ("Ignoring enabled modem '%s' as already have another one",
+                         path);
+                return;
+        }
 
-static void
-on_gps_refresh_rate_set (GObject      *source_object,
-                         GAsyncResult *res,
-                         gpointer      user_data)
-{
-        gboolean ret;
-        GError *error = NULL;
+        g_debug ("Modem '%s' now enabled", path);
 
-        ret = mm_modem_location_set_gps_refresh_rate_finish
-                (MM_MODEM_LOCATION (source_object), res, &error);
-        if (!ret) {
-                g_warning ("Failed to set GPS refresh rate: %s",
-                           error->message);
-                g_error_free (error);
+        mm_object = MM_OBJECT (g_dbus_object_manager_get_object (obj_manager, path));
+        g_assert (mm_object);
+
+        if (priv->mm_object == NULL) {
+                try_modem (manager, mm_object, mm_modem, TRUE);
+        } else {
+                /* MM re-initializes the location interface so we have to re-connect */
+                disconnect_modem_location (manager);
+                if (!try_modem_location (manager, mm_object)) {
+                        /* Notify that sadly GPS is no longer available */
+                        g_object_notify_by_pspec (G_OBJECT (manager), gParamSpecs[PROP_IS_GPS_AVAILABLE]);
+                        /* TODO: try next modem */
+                        return;
+                }
+
+                g_debug ("Enabling 3G and CDMA location on modem '%s'", path);
+                g_assert (manager->priv->modem_location);
+                g_object_notify_by_pspec (G_OBJECT (manager), gParamSpecs[PROP_IS_3G_AVAILABLE]);
+                g_object_notify_by_pspec (G_OBJECT (manager), gParamSpecs[PROP_IS_CDMA_AVAILABLE]);
+                enable_agps (manager);
         }
 }
 
@@ -675,52 +902,38 @@ on_mm_object_added (GDBusObjectManager *object_manager,
 {
         MMObject *mm_object = MM_OBJECT (object);
         GClueModemManager *manager = GCLUE_MODEM_MANAGER (user_data);
-        MMModem *mm_modem;
-        MMModemLocation *modem_location;
+        g_autoptr(MMModem) mm_modem = NULL;
+        const char *path = mm_object_get_path (mm_object);
+        gboolean modem_is_enabled;
 
-        if (manager->priv->mm_object != NULL)
+        if (manager->priv->mm_object != NULL) {
+                g_debug ("New modem '%s' but already have one", path);
                 return;
+        }
+
+        if (g_hash_table_lookup (manager->priv->modems_not_enabled, path)) {
+                g_warning ("New modem '%s' notification for an existing non-enabled modem",
+                           path);
+                return;
+        }
 
-        g_debug ("New modem '%s'", mm_object_get_path (mm_object));
+        g_debug ("New modem '%s'", path);
         mm_modem = mm_object_get_modem (mm_object);
-        if (mm_modem_get_state (mm_modem) < MM_MODEM_STATE_ENABLED) {
-                g_debug ("Modem '%s' not enabled",
-                         mm_object_get_path (mm_object));
+        modem_is_enabled = mm_modem_get_state (mm_modem) >= MM_MODEM_STATE_ENABLED;
+        if (!modem_is_enabled) {
+                g_debug ("Modem '%s' not enabled", path);
+
+                g_hash_table_insert (manager->priv->modems_not_enabled,
+                                     g_strdup (path), g_object_ref (mm_modem));
 
                 g_signal_connect_object (mm_modem,
                                          "notify::state",
                                          G_CALLBACK (on_mm_modem_state_notify),
                                          manager,
                                          0);
-
-                return;
         }
 
-        modem_location = mm_object_peek_modem_location (mm_object);
-        if (modem_location == NULL)
-                return;
-
-        g_debug ("Modem '%s' has location capabilities",
-                 mm_object_get_path (mm_object));
-
-        manager->priv->mm_object = g_object_ref (mm_object);
-        manager->priv->modem = mm_modem;
-        manager->priv->modem_location = mm_object_get_modem_location (mm_object);
-
-        mm_modem_location_set_gps_refresh_rate (manager->priv->modem_location,
-                                                manager->priv->time_threshold,
-                                                manager->priv->cancellable,
-                                                on_gps_refresh_rate_set,
-                                                NULL);
-
-        g_signal_connect (G_OBJECT (manager->priv->modem_location),
-                          "notify::location",
-                          G_CALLBACK (on_location_changed),
-                          manager);
-
-        g_object_notify_by_pspec (G_OBJECT (manager), gParamSpecs[PROP_IS_3G_AVAILABLE]);
-        g_object_notify_by_pspec (G_OBJECT (manager), gParamSpecs[PROP_IS_CDMA_AVAILABLE]);
-        g_object_notify_by_pspec (G_OBJECT (manager), gParamSpecs[PROP_IS_GPS_AVAILABLE]);
+        try_modem (manager, mm_object, mm_modem, modem_is_enabled);
 }
 
 static void
@@ -731,21 +944,34 @@ on_mm_object_removed (GDBusObjectManager *object_manager,
         MMObject *mm_object = MM_OBJECT (object);
         GClueModemManager *manager = GCLUE_MODEM_MANAGER (user_data);
         GClueModemManagerPrivate *priv = manager->priv;
+        const char *path = mm_object_get_path (priv->mm_object);
+
+        g_hash_table_remove (manager->priv->modems_not_enabled, path);
 
-        if (priv->mm_object == NULL || priv->mm_object != mm_object)
+        if (priv->mm_object == NULL || priv->mm_object != mm_object) {
+                g_debug ("Unused modem '%s' removed.", path);
                 return;
-        g_debug ("Modem '%s' removed.", mm_object_get_path (priv->mm_object));
+        }
+        g_debug ("Modem '%s' removed.", path);
 
-        g_signal_handlers_disconnect_by_func (G_OBJECT (priv->modem_location),
-                                              G_CALLBACK (on_location_changed),
+        clear_3gpp_location (manager);
+
+        disconnect_modem_location (manager);
+
+        g_signal_handlers_disconnect_by_func (G_OBJECT (priv->modem),
+                                              on_mm_modem_state_notify,
                                               user_data);
+
         g_clear_object (&priv->mm_object);
         g_clear_object (&priv->modem);
-        g_clear_object (&priv->modem_location);
+
+        priv->caps = 0;
 
         g_object_notify_by_pspec (G_OBJECT (manager), gParamSpecs[PROP_IS_3G_AVAILABLE]);
         g_object_notify_by_pspec (G_OBJECT (manager), gParamSpecs[PROP_IS_CDMA_AVAILABLE]);
         g_object_notify_by_pspec (G_OBJECT (manager), gParamSpecs[PROP_IS_GPS_AVAILABLE]);
+
+        /* TODO: try next modem */
 }
 
 static void
@@ -753,19 +979,27 @@ on_manager_new_ready (GObject      *modem_object,
                       GAsyncResult *res,
                       gpointer      user_data)
 {
-        GClueModemManagerPrivate *priv = GCLUE_MODEM_MANAGER (user_data)->priv;
+        MMManager *mmmanager;
+        GClueModemManager *manager;
+        GClueModemManagerPrivate *priv;
         GList *objects, *node;
-        GError *error = NULL;
-
-        priv->manager = mm_manager_new_finish (res, &error);
-        if (priv->manager == NULL) {
-                g_warning ("Failed to connect to ModemManager: %s",
-                           error->message);
-                g_error_free (error);
+        g_autoptr(GError) error = NULL;
+
+        mmmanager = mm_manager_new_finish (res, &error);
+        if (mmmanager == NULL) {
+                if (error && !g_error_matches (error, G_IO_ERROR,
+                                               G_IO_ERROR_CANCELLED)) {
+                        g_warning ("Failed to connect to ModemManager: %s",
+                                   error->message);
+                }
 
                 return;
         }
 
+        manager = GCLUE_MODEM_MANAGER (user_data);
+        priv = manager->priv;
+        priv->manager = mmmanager;
+
         objects = g_dbus_object_manager_get_objects
                         (G_DBUS_OBJECT_MANAGER (priv->manager));
         for (node = objects; node != NULL; node = node->next) {
@@ -779,15 +1013,15 @@ on_manager_new_ready (GObject      *modem_object,
         }
         g_list_free_full (objects, g_object_unref);
 
-        g_signal_connect (G_OBJECT (priv->manager),
-                          "object-added",
-                          G_CALLBACK (on_mm_object_added),
-                          user_data);
+        g_signal_connect_object (G_OBJECT (priv->manager),
+                                 "object-added",
+                                 G_CALLBACK (on_mm_object_added),
+                                 manager, 0);
 
-        g_signal_connect (G_OBJECT (priv->manager),
-                          "object-removed",
-                          G_CALLBACK (on_mm_object_removed),
-                          user_data);
+        g_signal_connect_object (G_OBJECT (priv->manager),
+                                 "object-removed",
+                                 G_CALLBACK (on_mm_object_removed),
+                                 manager, 0);
 }
 
 static void
@@ -795,19 +1029,22 @@ on_bus_get_ready (GObject      *modem_object,
                   GAsyncResult *res,
                   gpointer      user_data)
 {
-        GClueModemManagerPrivate *priv = GCLUE_MODEM_MANAGER (user_data)->priv;
-        GDBusConnection *connection;
-        GError *error = NULL;
+        GClueModemManagerPrivate *priv;
+        g_autoptr(GDBusConnection) connection = NULL;
+        g_autoptr(GError) error = NULL;
 
         connection = g_bus_get_finish (res, &error);
         if (connection == NULL) {
-                g_warning ("Failed to connect to system D-Bus: %s",
-                           error->message);
-                g_error_free (error);
+                if (error && !g_error_matches (error, G_IO_ERROR,
+                                               G_IO_ERROR_CANCELLED)) {
+                        g_warning ("Failed to connect to system D-Bus: %s",
+                                   error->message);
+                }
 
                 return;
         }
 
+        priv = GCLUE_MODEM_MANAGER (user_data)->priv;
         mm_manager_new (connection,
                         0,
                         priv->cancellable,
@@ -834,6 +1071,10 @@ static void
 gclue_modem_manager_init (GClueModemManager *manager)
 {
         manager->priv = gclue_modem_manager_get_instance_private (manager);
+        manager->priv->modems_not_enabled = g_hash_table_new_full (g_str_hash,
+                                                                   g_str_equal,
+                                                                   g_free,
+                                                                   g_object_unref);
 }
 
 static void
@@ -872,18 +1113,22 @@ static gboolean
 gclue_modem_manager_get_is_3g_available (GClueModem *modem)
 {
         g_return_val_if_fail (GCLUE_IS_MODEM_MANAGER (modem), FALSE);
+        GClueModemManager *manager = GCLUE_MODEM_MANAGER (modem);
 
-        return modem_has_caps (GCLUE_MODEM_MANAGER (modem),
-                                MM_MODEM_LOCATION_SOURCE_3GPP_LAC_CI);
+        return manager->priv->modem != NULL
+                && mm_modem_get_state (manager->priv->modem) >= MM_MODEM_STATE_ENABLED
+                && modem_has_caps (manager, MM_MODEM_LOCATION_SOURCE_3GPP_LAC_CI);
 }
 
 static gboolean
 gclue_modem_manager_get_is_cdma_available (GClueModem *modem)
 {
         g_return_val_if_fail (GCLUE_IS_MODEM_MANAGER (modem), FALSE);
+        GClueModemManager *manager = GCLUE_MODEM_MANAGER (modem);
 
-        return modem_has_caps (GCLUE_MODEM_MANAGER (modem),
-                               MM_MODEM_LOCATION_SOURCE_CDMA_BS);
+        return manager->priv->modem != NULL
+                && mm_modem_get_state (manager->priv->modem) >= MM_MODEM_STATE_ENABLED
+                && modem_has_caps (manager, MM_MODEM_LOCATION_SOURCE_CDMA_BS);
 }
 
 static gboolean
@@ -992,29 +1237,11 @@ gclue_modem_manager_enable_gps (GClueModem         *modem,
                                 GAsyncReadyCallback callback,
                                 gpointer            user_data)
 {
-        MMModemLocationSource assistance_caps;
-
         g_return_if_fail (GCLUE_IS_MODEM_MANAGER (modem));
         g_return_if_fail (gclue_modem_manager_get_is_gps_available (modem));
 
-        assistance_caps = MM_MODEM_LOCATION_SOURCE_NONE;
-#if MM_CHECK_VERSION(1, 12, 0)
-        /* Prefer MSB assistance */
-        if (modem_has_caps (GCLUE_MODEM_MANAGER (modem),
-                            MM_MODEM_LOCATION_SOURCE_AGPS_MSB)) {
-                assistance_caps |= MM_MODEM_LOCATION_SOURCE_AGPS_MSB;
-                g_debug ("Using MSB assisted GPS");
-        } else if (modem_has_caps (GCLUE_MODEM_MANAGER (modem),
-                                   MM_MODEM_LOCATION_SOURCE_AGPS_MSA)) {
-                assistance_caps |= MM_MODEM_LOCATION_SOURCE_AGPS_MSA;
-                g_debug ("Using MSA assisted GPS");
-        } else {
-                g_debug ("Assisted GPS not available");
-        }
-#endif
-
         enable_caps (GCLUE_MODEM_MANAGER (modem),
-                     MM_MODEM_LOCATION_SOURCE_GPS_NMEA | assistance_caps,
+                     MM_MODEM_LOCATION_SOURCE_GPS_NMEA,
                      cancellable,
                      callback,
                      user_data);
@@ -1043,7 +1270,7 @@ gclue_modem_manager_disable_3g (GClueModem   *modem,
         g_return_val_if_fail (gclue_modem_manager_get_is_3g_available (modem), FALSE);
         manager = GCLUE_MODEM_MANAGER (modem);
 
-        g_clear_object (&manager->priv->location_3gpp);
+        clear_3gpp_location (manager);
         g_debug ("Clearing 3GPP location caps from modem");
         return clear_caps (manager,
                            MM_MODEM_LOCATION_SOURCE_3GPP_LAC_CI,
@@ -1062,7 +1289,7 @@ gclue_modem_manager_disable_cdma (GClueModem   *modem,
         g_return_val_if_fail (gclue_modem_manager_get_is_cdma_available (modem), FALSE);
         manager = GCLUE_MODEM_MANAGER (modem);
 
-        g_clear_object (&manager->priv->location_3gpp);
+        clear_3gpp_location (manager);
         g_debug ("Clearing CDMA location caps from modem");
         return clear_caps (manager,
                            MM_MODEM_LOCATION_SOURCE_CDMA_BS,
@@ -1077,22 +1304,15 @@ gclue_modem_manager_disable_gps (GClueModem   *modem,
                                  GError      **error)
 {
         GClueModemManager *manager;
-        MMModemLocationSource assistance_caps;
 
         g_return_val_if_fail (GCLUE_IS_MODEM_MANAGER (modem), FALSE);
         g_return_val_if_fail (gclue_modem_manager_get_is_gps_available (modem), FALSE);
         manager = GCLUE_MODEM_MANAGER (modem);
 
-#if MM_CHECK_VERSION(1, 12, 0)
-        assistance_caps = manager->priv->caps & (MM_MODEM_LOCATION_SOURCE_AGPS_MSA | MM_MODEM_LOCATION_SOURCE_AGPS_MSB);
-#else
-        assistance_caps = MM_MODEM_LOCATION_SOURCE_NONE;
-#endif
-
         g_clear_object (&manager->priv->location_nmea);
         g_debug ("Clearing GPS NMEA caps from modem");
         return clear_caps (manager,
-                           MM_MODEM_LOCATION_SOURCE_GPS_NMEA | assistance_caps,
+                           MM_MODEM_LOCATION_SOURCE_GPS_NMEA,
                            cancellable,
                            error);
 }
diff --git a/src/gclue-mozilla.c b/src/gclue-mozilla.c
index 8e602c1..9e8feb1 100644
--- a/src/gclue-mozilla.c
+++ b/src/gclue-mozilla.c
@@ -25,8 +25,10 @@
 #include <string.h>
 #include <config.h>
 #include "gclue-mozilla.h"
+#include "gclue-3g-tower.h"
 #include "gclue-config.h"
 #include "gclue-error.h"
+#include "gclue-wifi.h"
 
 /**
  * SECTION:gclue-mozilla
@@ -40,6 +42,22 @@
  * its easy to switch to Google's API.
  **/
 
+struct _GClueMozillaPrivate
+{
+        GClueWifi *wifi;
+
+        GClue3GTower tower;
+        gboolean tower_valid;
+        gboolean tower_submitted;
+
+        gboolean bss_submitted;
+};
+
+G_DEFINE_TYPE_WITH_CODE (GClueMozilla,
+                         gclue_mozilla,
+                         G_TYPE_OBJECT,
+                         G_ADD_PRIVATE (GClueMozilla))
+
 #define BSSID_LEN 6
 #define BSSID_STR_LEN 17
 #define MAX_SSID_LEN 32
@@ -102,12 +120,10 @@ get_bssid_from_bss (WPABSS *bss, char *bssid)
         return TRUE;
 }
 
-static const char *
-get_url (void)
+const char *
+gclue_mozilla_get_locate_url (GClueMozilla *mozilla)
 {
-        GClueConfig *config;
-
-        config = gclue_config_get_singleton ();
+        GClueConfig *config = gclue_config_get_singleton ();
 
         return gclue_config_get_wifi_url (config);
 }
@@ -136,25 +152,58 @@ error:
         return FALSE;
 }
 
+static gboolean
+towertec_to_radiotype (GClueTowerTec tec,
+                       const char **radiotype_p)
+{
+        switch (tec) {
+        case GCLUE_TOWER_TEC_2G:
+            *radiotype_p = "gsm";
+            break;
+        case GCLUE_TOWER_TEC_3G:
+            *radiotype_p = "wcdma";
+            break;
+        case GCLUE_TOWER_TEC_4G:
+            *radiotype_p = "lte";
+            break;
+        default:
+            *radiotype_p = NULL;
+            return FALSE;
+        }
+
+        return TRUE;
+}
+
+#define USER_AGENT (PACKAGE_NAME "/" PACKAGE_VERSION)
+
 SoupMessage *
-gclue_mozilla_create_query (GList        *bss_list, /* As in Access Points */
-                            GClue3GTower *tower,
+gclue_mozilla_create_query (GClueMozilla  *mozilla,
+                            gboolean skip_tower,
+                            gboolean skip_bss,
+                            const char **query_data_description,
                             GError      **error)
 {
+        gboolean has_tower = FALSE, has_bss = FALSE;
         SoupMessage *ret = NULL;
+        SoupMessageHeaders *request_headers;
         JsonBuilder *builder;
+        g_autoptr(GList) bss_list = NULL;
         JsonGenerator *generator;
         JsonNode *root_node;
         char *data;
         gsize data_len;
-        const char *uri;
+        const char *uri, *radiotype;
         guint n_non_ignored_bsss;
         GList *iter;
         gint64 mcc, mnc;
+        g_autoptr(GBytes) body = NULL;
 
         builder = json_builder_new ();
         json_builder_begin_object (builder);
 
+        if (mozilla->priv->wifi && !skip_bss) {
+                bss_list = gclue_wifi_get_bss_list (mozilla->priv->wifi);
+        }
         /* We send pure geoip query using empty object if both bss_list and
          * tower are NULL.
          *
@@ -172,11 +221,11 @@ gclue_mozilla_create_query (GList        *bss_list, /* As in Access Points */
                 n_non_ignored_bsss++;
         }
 
-        if (tower != NULL &&
-            operator_code_to_mcc_mnc (tower->opc, &mcc, &mnc)) {
-
+        if (mozilla->priv->tower_valid && !skip_tower &&
+            towertec_to_radiotype (mozilla->priv->tower.tec, &radiotype) &&
+            operator_code_to_mcc_mnc (mozilla->priv->tower.opc, &mcc, &mnc)) {
                 json_builder_set_member_name (builder, "radioType");
-                json_builder_add_string_value (builder, "gsm");
+                json_builder_add_string_value (builder, radiotype);
 
                 json_builder_set_member_name (builder, "cellTowers");
                 json_builder_begin_array (builder);
@@ -184,21 +233,21 @@ gclue_mozilla_create_query (GList        *bss_list, /* As in Access Points */
                 json_builder_begin_object (builder);
 
                 json_builder_set_member_name (builder, "cellId");
-                json_builder_add_int_value (builder, tower->cell_id);
+                json_builder_add_int_value (builder, mozilla->priv->tower.cell_id);
                 json_builder_set_member_name (builder, "mobileCountryCode");
                 json_builder_add_int_value (builder, mcc);
                 json_builder_set_member_name (builder, "mobileNetworkCode");
                 json_builder_add_int_value (builder, mnc);
                 json_builder_set_member_name (builder, "locationAreaCode");
-                json_builder_add_int_value (builder, tower->lac);
-                if (tower->tec == GCLUE_TOWER_TEC_4G) {
-                        json_builder_set_member_name (builder, "radioType");
-                        json_builder_add_string_value (builder, "lte");
-		}
+                json_builder_add_int_value (builder, mozilla->priv->tower.lac);
+                json_builder_set_member_name (builder, "radioType");
+                json_builder_add_string_value (builder, radiotype);
 
                 json_builder_end_object (builder);
 
                 json_builder_end_array (builder);
+
+                has_tower = TRUE;
         }
 
         if (n_non_ignored_bsss >= 2) {
@@ -209,11 +258,13 @@ gclue_mozilla_create_query (GList        *bss_list, /* As in Access Points */
                         WPABSS *bss = WPA_BSS (iter->data);
                         char mac[BSSID_STR_LEN + 1] = { 0 };
                         gint16 strength_dbm;
+                        guint age_ms;
 
                         if (gclue_mozilla_should_ignore_bss (bss))
                                 continue;
 
                         json_builder_begin_object (builder);
+
                         json_builder_set_member_name (builder, "macAddress");
                         get_bssid_from_bss (bss, mac);
                         json_builder_add_string_value (builder, mac);
@@ -221,7 +272,13 @@ gclue_mozilla_create_query (GList        *bss_list, /* As in Access Points */
                         json_builder_set_member_name (builder, "signalStrength");
                         strength_dbm = wpa_bss_get_signal (bss);
                         json_builder_add_int_value (builder, strength_dbm);
+
+                        json_builder_set_member_name (builder, "age");
+                        age_ms = 1000 * wpa_bss_get_age (bss);
+                        json_builder_add_int_value (builder, age_ms);
+
                         json_builder_end_object (builder);
+                        has_bss = TRUE;
                 }
                 json_builder_end_array (builder);
         }
@@ -236,15 +293,26 @@ gclue_mozilla_create_query (GList        *bss_list, /* As in Access Points */
         g_object_unref (builder);
         g_object_unref (generator);
 
-        uri = get_url ();
+        uri = gclue_mozilla_get_locate_url (mozilla);
         ret = soup_message_new ("POST", uri);
-        soup_message_set_request (ret,
-                                  "application/json",
-                                  SOUP_MEMORY_TAKE,
-                                  data,
-                                  data_len);
+        request_headers = soup_message_get_request_headers (ret);
+        soup_message_headers_append (request_headers, "User-Agent", USER_AGENT);
+        body = g_bytes_new_take (data, data_len);
+        soup_message_set_request_body_from_bytes (ret, "application/json", body);
         g_debug ("Sending following request to '%s':\n%s", uri, data);
 
+        if (query_data_description) {
+                if (has_tower && has_bss) {
+                        *query_data_description = "3GPP + WiFi";
+                } else if (has_tower) {
+                        *query_data_description = "3GPP";
+                } else if (has_bss) {
+                        *query_data_description = "WiFi";
+                } else {
+                        *query_data_description = "GeoIP";
+                }
+        }
+
         return ret;
 }
 
@@ -252,28 +320,32 @@ static gboolean
 parse_server_error (JsonObject *object, GError **error)
 {
         JsonObject *error_obj;
-        int code;
         const char *message;
 
         if (!json_object_has_member (object, "error"))
             return FALSE;
 
         error_obj = json_object_get_object_member (object, "error");
-        code = json_object_get_int_member (error_obj, "code");
-        message = json_object_get_string_member (error_obj, "message");
+        if (json_object_has_member (error_obj, "message")) {
+                message = json_object_get_string_member (error_obj, "message");
+        } else {
+                message = "Unknown error";
+        }
 
-        g_set_error_literal (error, G_IO_ERROR, code, message);
+        g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED, message);
 
         return TRUE;
 }
 
 GClueLocation *
 gclue_mozilla_parse_response (const char *json,
+                              const char *location_description,
                               GError    **error)
 {
-        JsonParser *parser;
+        g_autoptr(JsonParser) parser = NULL;
         JsonNode *node;
         JsonObject *object, *loc_object;
+        g_autofree char *desc_new = NULL;
         GClueLocation *location;
         gdouble latitude, longitude, accuracy;
 
@@ -288,54 +360,78 @@ gclue_mozilla_parse_response (const char *json,
         if (parse_server_error (object, error))
                 return NULL;
 
+        if (json_object_has_member (object, "fallback")) {
+                const char *fallback;
+
+                fallback = json_object_get_string_member (object, "fallback");
+                if (fallback && strlen (fallback)) {
+                        desc_new = g_strdup_printf ("%s fallback (from %s data)",
+                                                    fallback, location_description);
+                        location_description = desc_new;
+                }
+        }
+
         loc_object = json_object_get_object_member (object, "location");
         latitude = json_object_get_double_member (loc_object, "lat");
         longitude = json_object_get_double_member (loc_object, "lng");
 
         accuracy = json_object_get_double_member (object, "accuracy");
 
-        location = gclue_location_new (latitude, longitude, accuracy);
-
-        g_object_unref (parser);
+        location = gclue_location_new (latitude, longitude, accuracy,
+                                       location_description);
 
         return location;
 }
 
-static const char *
-get_submit_config (const char **nick)
+const char *
+gclue_mozilla_get_submit_url (GClueMozilla *mozilla)
 {
-        GClueConfig *config;
+        GClueConfig *config = gclue_config_get_singleton ();
 
-        config = gclue_config_get_singleton ();
-        if (!gclue_config_get_wifi_submit_data (config))
+        if (gclue_config_get_wifi_submit_data (config))
+                return gclue_config_get_wifi_submit_url (config);
+        else
                 return NULL;
-
-        *nick = gclue_config_get_wifi_submit_nick (config);
-
-        return gclue_config_get_wifi_submit_url (config);
 }
 
 SoupMessage *
-gclue_mozilla_create_submit_query (GClueLocation   *location,
-                                   GList           *bss_list, /* As in Access Points */
-                                   GClue3GTower    *tower,
+gclue_mozilla_create_submit_query (GClueMozilla  *mozilla,
+                                   GClueLocation   *location,
                                    GError         **error)
 {
         SoupMessage *ret = NULL;
+        SoupMessageHeaders *request_headers;
         JsonBuilder *builder;
         JsonGenerator *generator;
         JsonNode *root_node;
-        char *data, *timestr;
-        const char *url, *nick;
+        char *data;
+        g_autoptr(GList) bss_list = NULL;
+        const char *url, *nick, *radiotype;
         gsize data_len;
         GList *iter;
-        gdouble lat, lon, accuracy, altitude;
-        GDateTime *datetime;
+        gdouble lat, lon, accuracy, altitude, speed;
+        guint64 time_ms;
         gint64 mcc, mnc;
+        GClueConfig *config;
+        g_autoptr(GBytes) body = NULL;
+
+        if (mozilla->priv->bss_submitted &&
+            (!mozilla->priv->tower_valid ||
+             mozilla->priv->tower_submitted))
+        {
+                g_debug ("Already created submit req for this data (bss submitted %d; tower: valid %d submitted %d)",
+                         (int)mozilla->priv->bss_submitted,
+                         (int)mozilla->priv->tower_valid,
+                         (int)mozilla->priv->tower_submitted);
+                goto out;
+        }
 
-        url = get_submit_config (&nick);
+
+        url = gclue_mozilla_get_submit_url (mozilla);
         if (url == NULL)
                 goto out;
+        config = gclue_config_get_singleton ();
+        nick = gclue_config_get_wifi_submit_nick (config);
 
         builder = json_builder_new ();
         json_builder_begin_object (builder);
@@ -345,12 +441,19 @@ gclue_mozilla_create_submit_query (GClueLocation   *location,
 
         json_builder_begin_object (builder);
 
+        json_builder_set_member_name (builder, "timestamp");
+        time_ms = 1000 * gclue_location_get_timestamp (location);
+        json_builder_add_int_value (builder, time_ms);
+
+        json_builder_set_member_name (builder, "position");
+        json_builder_begin_object (builder);
+
         lat = gclue_location_get_latitude (location);
-        json_builder_set_member_name (builder, "lat");
+        json_builder_set_member_name (builder, "latitude");
         json_builder_add_double_value (builder, lat);
 
         lon = gclue_location_get_longitude (location);
-        json_builder_set_member_name (builder, "lon");
+        json_builder_set_member_name (builder, "longitude");
         json_builder_add_double_value (builder, lon);
 
         accuracy = gclue_location_get_accuracy (location);
@@ -365,22 +468,19 @@ gclue_mozilla_create_submit_query (GClueLocation   *location,
                 json_builder_add_double_value (builder, altitude);
         }
 
-        datetime = g_date_time_new_from_unix_local
-                (gclue_location_get_timestamp (location));
-        /* We need to be compatible with GLib 2.56 so we cannot use this:
-         * timestr = g_date_time_format_iso8601 (datetime);
-         * Construct the format manually instead: */
-        timestr = g_date_time_format (datetime, "%Y-%m-%dT%H:%M:%S%:::z");
-        json_builder_set_member_name (builder, "time");
-        json_builder_add_string_value (builder, timestr);
-        g_free (timestr);
-        g_date_time_unref (datetime);
+        speed = gclue_location_get_speed (location);
+        if (speed != GCLUE_LOCATION_SPEED_UNKNOWN) {
+                json_builder_set_member_name (builder, "speed");
+                json_builder_add_double_value (builder, speed);
+        }
 
-        json_builder_set_member_name (builder, "radioType");
-        json_builder_add_string_value (builder, "gsm");
+        json_builder_end_object (builder); /* position */
 
+        if (mozilla->priv->wifi) {
+                bss_list = gclue_wifi_get_bss_list (mozilla->priv->wifi);
+        }
         if (bss_list != NULL) {
-                json_builder_set_member_name (builder, "wifi");
+                json_builder_set_member_name (builder, "wifiAccessPoints");
                 json_builder_begin_array (builder);
 
                 for (iter = bss_list; iter != NULL; iter = iter->next) {
@@ -388,50 +488,57 @@ gclue_mozilla_create_submit_query (GClueLocation   *location,
                         char mac[BSSID_STR_LEN + 1] = { 0 };
                         gint16 strength_dbm;
                         guint16 frequency;
+                        guint age_ms;
 
                         if (gclue_mozilla_should_ignore_bss (bss))
                                 continue;
 
                         json_builder_begin_object (builder);
-                        json_builder_set_member_name (builder, "key");
+
+                        json_builder_set_member_name (builder, "macAddress");
                         get_bssid_from_bss (bss, mac);
                         json_builder_add_string_value (builder, mac);
 
-                        json_builder_set_member_name (builder, "signal");
+                        json_builder_set_member_name (builder, "signalStrength");
                         strength_dbm = wpa_bss_get_signal (bss);
                         json_builder_add_int_value (builder, strength_dbm);
 
                         json_builder_set_member_name (builder, "frequency");
                         frequency = wpa_bss_get_frequency (bss);
                         json_builder_add_int_value (builder, frequency);
+
+                        json_builder_set_member_name (builder, "age");
+                        age_ms = 1000 * wpa_bss_get_age (bss);
+                        json_builder_add_int_value (builder, age_ms);
+
                         json_builder_end_object (builder);
                 }
 
-                json_builder_end_array (builder); /* wifi */
+                json_builder_end_array (builder); /* wifiAccessPoints */
         }
 
-        if (tower != NULL &&
-            operator_code_to_mcc_mnc (tower->opc, &mcc, &mnc)) {
-
-                json_builder_set_member_name (builder, "cell");
+        if (mozilla->priv->tower_valid &&
+            towertec_to_radiotype (mozilla->priv->tower.tec, &radiotype) &&
+            operator_code_to_mcc_mnc (mozilla->priv->tower.opc, &mcc, &mnc)) {
+                json_builder_set_member_name (builder, "cellTowers");
                 json_builder_begin_array (builder);
 
                 json_builder_begin_object (builder);
 
-                json_builder_set_member_name (builder, "radio");
-                json_builder_add_string_value (builder, "gsm");
-                json_builder_set_member_name (builder, "cid");
-                json_builder_add_int_value (builder, tower->cell_id);
-                json_builder_set_member_name (builder, "mcc");
+                json_builder_set_member_name (builder, "radioType");
+                json_builder_add_string_value (builder, radiotype);
+                json_builder_set_member_name (builder, "cellId");
+                json_builder_add_int_value (builder, mozilla->priv->tower.cell_id);
+                json_builder_set_member_name (builder, "mobileCountryCode");
                 json_builder_add_int_value (builder, mcc);
-                json_builder_set_member_name (builder, "mnc");
+                json_builder_set_member_name (builder, "mobileNetworkCode");
                 json_builder_add_int_value (builder, mnc);
-                json_builder_set_member_name (builder, "lac");
-                json_builder_add_int_value (builder, tower->lac);
+                json_builder_set_member_name (builder, "locationAreaCode");
+                json_builder_add_int_value (builder, mozilla->priv->tower.lac);
 
                 json_builder_end_object (builder);
 
-                json_builder_end_array (builder); /* cell */
+                json_builder_end_array (builder); /* cellTowers */
         }
 
         json_builder_end_object (builder);
@@ -448,17 +555,19 @@ gclue_mozilla_create_submit_query (GClueLocation   *location,
         g_object_unref (generator);
 
         ret = soup_message_new ("POST", url);
+        request_headers = soup_message_get_request_headers (ret);
+        soup_message_headers_append (request_headers, "User-Agent", USER_AGENT);
         if (nick != NULL && nick[0] != '\0')
-                soup_message_headers_append (ret->request_headers,
+                soup_message_headers_append (request_headers,
                                              "X-Nickname",
                                              nick);
-        soup_message_set_request (ret,
-                                  "application/json",
-                                  SOUP_MEMORY_TAKE,
-                                  data,
-                                  data_len);
+        body = g_bytes_new_take (data, data_len);
+        soup_message_set_request_body_from_bytes (ret, "application/json", body);
         g_debug ("Sending following request to '%s':\n%s", url, data);
 
+        mozilla->priv->bss_submitted = TRUE;
+        mozilla->priv->tower_submitted = TRUE;
+
 out:
         return ret;
 }
@@ -485,3 +594,134 @@ gclue_mozilla_should_ignore_bss (WPABSS *bss)
 
         return FALSE;
 }
+
+static void
+gclue_mozilla_finalize (GObject *object)
+{
+        GClueMozilla *mozilla = GCLUE_MOZILLA (object);
+
+        g_clear_weak_pointer (&mozilla->priv->wifi);
+
+        G_OBJECT_CLASS (gclue_mozilla_parent_class)->finalize (object);
+}
+
+static void
+gclue_mozilla_init (GClueMozilla *mozilla)
+{
+        mozilla->priv = gclue_mozilla_get_instance_private (mozilla);
+        mozilla->priv->wifi = NULL;
+        mozilla->priv->tower_valid = FALSE;
+        mozilla->priv->bss_submitted = FALSE;
+}
+
+static void
+gclue_mozilla_class_init (GClueMozillaClass *klass)
+{
+        GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+        object_class->finalize = gclue_mozilla_finalize;
+}
+
+GClueMozilla *
+gclue_mozilla_get_singleton (void)
+{
+        static GClueMozilla *mozilla = NULL;
+
+        if (!mozilla) {
+                mozilla = g_object_new (GCLUE_TYPE_MOZILLA, NULL);
+                g_object_add_weak_pointer (G_OBJECT (mozilla), (gpointer) &mozilla);
+        } else
+                g_object_ref (mozilla);
+
+        return mozilla;
+}
+
+void
+gclue_mozilla_set_wifi (GClueMozilla *mozilla,
+                        GClueWifi *wifi)
+{
+        g_return_if_fail (GCLUE_IS_MOZILLA (mozilla));
+
+        if (mozilla->priv->wifi == wifi)
+                return;
+
+        g_clear_weak_pointer (&mozilla->priv->wifi);
+
+        if (!wifi) {
+                return;
+        }
+
+        mozilla->priv->wifi = wifi;
+        g_object_add_weak_pointer (G_OBJECT (mozilla->priv->wifi),
+                                   (gpointer) &mozilla->priv->wifi);
+}
+
+gboolean
+gclue_mozilla_test_set_wifi (GClueMozilla *mozilla,
+                             GClueWifi *old, GClueWifi *new)
+{
+        if (mozilla->priv->wifi != old)
+                return FALSE;
+
+        gclue_mozilla_set_wifi (mozilla, new);
+        return TRUE;
+}
+
+void
+gclue_mozilla_set_bss_dirty (GClueMozilla *mozilla)
+{
+        g_return_if_fail (GCLUE_IS_MOZILLA (mozilla));
+
+        mozilla->priv->bss_submitted = FALSE;
+}
+
+static gboolean gclue_mozilla_tower_identical (const GClue3GTower *t1,
+                                               const GClue3GTower *t2)
+{
+        return g_strcmp0 (t1->opc, t2->opc) == 0 && t1->lac == t2->lac &&
+                t1->cell_id == t2->cell_id && t1->tec == t2->tec;
+}
+
+void
+gclue_mozilla_set_tower (GClueMozilla *mozilla,
+                         const GClue3GTower *tower)
+{
+        g_return_if_fail (GCLUE_IS_MOZILLA (mozilla));
+
+        if (!tower ||
+            !(tower->tec > GCLUE_TOWER_TEC_UNKNOWN
+              && tower->tec <= GCLUE_TOWER_TEC_MAX_VALID)) {
+                mozilla->priv->tower_valid = FALSE;
+                return;
+        }
+
+        if (mozilla->priv->tower_valid &&
+            mozilla->priv->tower_submitted) {
+                mozilla->priv->tower_submitted =
+                        gclue_mozilla_tower_identical (&mozilla->priv->tower,
+                                                       tower);
+        } else
+                mozilla->priv->tower_submitted = FALSE;
+
+        mozilla->priv->tower = *tower;
+        mozilla->priv->tower_valid = TRUE;
+}
+
+gboolean
+gclue_mozilla_has_tower (GClueMozilla *mozilla)
+{
+        g_return_val_if_fail (GCLUE_IS_MOZILLA (mozilla), FALSE);
+
+        return mozilla->priv->tower_valid;
+}
+
+GClue3GTower *
+gclue_mozilla_get_tower (GClueMozilla *mozilla)
+{
+        g_return_val_if_fail (GCLUE_IS_MOZILLA (mozilla), NULL);
+
+        if (!mozilla->priv->tower_valid)
+                return NULL;
+
+        return &mozilla->priv->tower;
+}
diff --git a/src/gclue-mozilla.h b/src/gclue-mozilla.h
index db859e5..a8010b0 100644
--- a/src/gclue-mozilla.h
+++ b/src/gclue-mozilla.h
@@ -30,21 +30,72 @@
 
 G_BEGIN_DECLS
 
+#define GCLUE_TYPE_MOZILLA            (gclue_mozilla_get_type())
+#define GCLUE_MOZILLA(obj)            (G_TYPE_CHECK_INSTANCE_CAST ((obj), GCLUE_TYPE_MOZILLA, GClueMozilla))
+#define GCLUE_MOZILLA_CONST(obj)      (G_TYPE_CHECK_INSTANCE_CAST ((obj), GCLUE_TYPE_MOZILLA, GClueMozilla const))
+#define GCLUE_MOZILLA_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST ((klass),  GCLUE_TYPE_MOZILLA, GClueMozillaClass))
+#define GCLUE_IS_MOZILLA(obj)         (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GCLUE_TYPE_MOZILLA))
+#define GCLUE_IS_MOZILLA_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass),  GCLUE_TYPE_MOZILLA))
+#define GCLUE_MOZILLA_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS ((obj),  GCLUE_TYPE_MOZILLA, GClueMozillaClass))
+
+typedef struct _GClueMozilla        GClueMozilla;
+typedef struct _GClueMozillaClass   GClueMozillaClass;
+typedef struct _GClueMozillaPrivate GClueMozillaPrivate;
+
+struct _GClueMozilla
+{
+        GObject parent;
+
+        /*< private >*/
+        GClueMozillaPrivate *priv;
+};
+
+struct _GClueMozillaClass
+{
+        GObjectClass parent_class;
+};
+
+struct GClueWifi;
+typedef struct _GClueWifi GClueWifi;
+
+GType gclue_mozilla_get_type (void) G_GNUC_CONST;
+
+GClueMozilla *gclue_mozilla_get_singleton (void);
+
+void gclue_mozilla_set_wifi (GClueMozilla *mozilla,
+                             GClueWifi *wifi);
+gboolean
+gclue_mozilla_test_set_wifi (GClueMozilla *mozilla,
+                             GClueWifi *old, GClueWifi *new);
+void gclue_mozilla_set_bss_dirty (GClueMozilla *mozilla);
+
+void gclue_mozilla_set_tower (GClueMozilla *mozilla,
+                              const GClue3GTower *tower);
+gboolean
+gclue_mozilla_has_tower (GClueMozilla *mozilla);
+GClue3GTower *
+gclue_mozilla_get_tower (GClueMozilla *mozilla);
+
 SoupMessage *
-gclue_mozilla_create_query (GList        *bss_list, /* As in Access Points */
-                            GClue3GTower *tower,
+gclue_mozilla_create_query (GClueMozilla  *mozilla,
+                            gboolean skip_tower,
+                            gboolean skip_bss,
+                            const char **query_data_description,
                             GError      **error);
 GClueLocation *
 gclue_mozilla_parse_response (const char *json,
+                              const char *location_description,
                               GError    **error);
 SoupMessage *
-gclue_mozilla_create_submit_query (GClueLocation   *location,
-                                   GList           *bss_list, /* As in Access Points */
-                                   GClue3GTower    *tower,
+gclue_mozilla_create_submit_query (GClueMozilla  *mozilla,
+                                   GClueLocation   *location,
                                    GError         **error);
 gboolean
 gclue_mozilla_should_ignore_bss (WPABSS *bss);
 
+const char *gclue_mozilla_get_locate_url (GClueMozilla *mozilla);
+const char *gclue_mozilla_get_submit_url (GClueMozilla *mozilla);
+
 G_END_DECLS
 
 #endif /* GCLUE_MOZILLA_H */
diff --git a/src/gclue-nmea-source.c b/src/gclue-nmea-source.c
index 68dbf29..13ada66 100644
--- a/src/gclue-nmea-source.c
+++ b/src/gclue-nmea-source.c
@@ -22,11 +22,13 @@
  */
 
 #include <stdlib.h>
+#include <string.h>
 #include <glib.h>
 #include "gclue-config.h"
+#include "gclue-location.h"
 #include "gclue-nmea-utils.h"
 #include "gclue-nmea-source.h"
-#include "gclue-location.h"
+#include "gclue-utils.h"
 #include "config.h"
 #include "gclue-enum-types.h"
 
@@ -37,21 +39,35 @@
 #include <avahi-glib/glib-watch.h>
 #include <gio/gunixsocketaddress.h>
 
+/* Once we run out of NMEA services to try how long to wait
+ * until retrying all of them.
+ * In seconds.
+ */
+#define SERVICE_UNBREAK_TIME 5
+
 typedef struct AvahiServiceInfo AvahiServiceInfo;
 
 struct _GClueNMEASourcePrivate {
         GSocketConnection *connection;
+        GDataInputStream *input_stream;
 
         GSocketClient *client;
 
         GCancellable *cancellable;
 
+        AvahiGLibPoll *glib_poll;
+
         AvahiClient *avahi_client;
 
         AvahiServiceInfo *active_service;
 
-        /* List of all services but only the most accurate one is used. */
-        GList *all_services;
+        /* List of services to try but only the most accurate one is used. */
+        GList *try_services;
+
+        /* List of known-broken services. */
+        GList *broken_services;
+
+        guint accuracy_refresh_source, unbreak_timer;
 };
 
 G_DEFINE_TYPE_WITH_CODE (GClueNMEASource,
@@ -65,16 +81,15 @@ static GClueLocationSourceStopResult
 gclue_nmea_source_stop (GClueLocationSource *source);
 
 static void
-connect_to_service (GClueNMEASource *source);
-static void
-disconnect_from_service (GClueNMEASource *source);
+try_connect_to_service (GClueNMEASource *source);
 
 struct AvahiServiceInfo {
     char *identifier;
     char *host_name;
+    gboolean is_socket;
     guint16 port;
     GClueAccuracyLevel accuracy;
-    guint64 timestamp;
+    gint64 timestamp_add;
 };
 
 static void
@@ -99,7 +114,7 @@ avahi_service_new (const char        *identifier,
         service->host_name = g_strdup (host_name);
         service->port = port;
         service->accuracy = accuracy;
-        service->timestamp = g_get_real_time () / G_USEC_PER_SEC;
+        service->timestamp_add = g_get_monotonic_time ();
 
         return service;
 }
@@ -122,16 +137,41 @@ compare_avahi_service_by_accuracy_n_time (gconstpointer a,
 {
         AvahiServiceInfo *first, *second;
         gint diff;
+        gint64 tdiff;
 
         first = (AvahiServiceInfo *) a;
         second = (AvahiServiceInfo *) b;
 
         diff = second->accuracy - first->accuracy;
+        if (diff)
+                return diff;
+
+        g_assert (first->timestamp_add >= 0);
+        g_assert (second->timestamp_add >= 0);
+        tdiff = first->timestamp_add - second->timestamp_add;
+        if (tdiff < 0)
+                return -1;
+        else if (tdiff > 0)
+                return 1;
+        else
+                return 0;
+}
+
+static void
+disconnect_from_service (GClueNMEASource *source)
+{
+        GClueNMEASourcePrivate *priv = source->priv;
 
-        if (diff == 0)
-                return first->timestamp - second->timestamp;
+        if (!priv->active_service)
+                return;
 
-        return diff;
+        g_cancellable_cancel (priv->cancellable);
+
+        g_clear_object (&priv->input_stream);
+        g_clear_object (&priv->connection);
+        g_clear_object (&priv->client);
+        g_clear_object (&priv->cancellable);
+        priv->active_service = NULL;
 }
 
 static gboolean
@@ -145,9 +185,9 @@ reconnection_required (GClueNMEASource *source)
          * 2. a more accurate service than one currently in use, is now
          *    available.
          */
-        return (priv->active_service != NULL &&
-                (priv->all_services == NULL ||
-                 priv->active_service != priv->all_services->data));
+        return priv->active_service == NULL ||
+                priv->try_services == NULL ||
+                priv->active_service != priv->try_services->data;
 }
 
 static void
@@ -157,25 +197,35 @@ reconnect_service (GClueNMEASource *source)
                 return;
 
         disconnect_from_service (source);
-        connect_to_service (source);
+        try_connect_to_service (source);
 }
 
-static void
-refresh_accuracy_level (GClueNMEASource *source)
+static GClueAccuracyLevel get_head_accuracy (GList *list)
+{
+        AvahiServiceInfo *service;
+
+        if (!list)
+                return GCLUE_ACCURACY_LEVEL_NONE;
+
+        service = (AvahiServiceInfo *) list->data;
+        return service->accuracy;
+}
+
+static gboolean
+on_refresh_accuracy_level (gpointer user_data)
 {
-        GClueAccuracyLevel new, existing;
+        GClueNMEASource *source = GCLUE_NMEA_SOURCE (user_data);
+        GClueNMEASourcePrivate *priv = source->priv;
+        GClueAccuracyLevel new_try, new_broken, new, existing;
+
+        priv->accuracy_refresh_source = 0;
 
         existing = gclue_location_source_get_available_accuracy_level
                         (GCLUE_LOCATION_SOURCE (source));
 
-        if (source->priv->all_services != NULL) {
-                AvahiServiceInfo *service;
-
-                service = (AvahiServiceInfo *) source->priv->all_services->data;
-                new = service->accuracy;
-        } else {
-                new = GCLUE_ACCURACY_LEVEL_NONE;
-        }
+        new_try = get_head_accuracy (priv->try_services);
+        new_broken = get_head_accuracy (priv->broken_services);
+        new = MAX (new_try, new_broken);
 
         if (new != existing) {
                 g_debug ("Available accuracy level from %s: %u",
@@ -184,6 +234,109 @@ refresh_accuracy_level (GClueNMEASource *source)
                               "available-accuracy-level", new,
                               NULL);
         }
+
+        return G_SOURCE_REMOVE;
+}
+
+static void
+refresh_accuracy_level (GClueNMEASource *source)
+{
+        GClueNMEASourcePrivate *priv = source->priv;
+
+        if (priv->accuracy_refresh_source) {
+                return;
+        }
+
+        g_debug ("Scheduling NMEA accuracy level refresh");
+        priv->accuracy_refresh_source = g_idle_add (on_refresh_accuracy_level,
+                                                    source);
+}
+
+static gboolean
+on_service_unbreak_time (gpointer source)
+{
+        GClueNMEASourcePrivate *priv = GCLUE_NMEA_SOURCE (source)->priv;
+
+        priv->unbreak_timer = 0;
+
+        if (!priv->try_services && priv->broken_services) {
+                g_debug ("Unbreaking existing NMEA services");
+
+                priv->try_services = priv->broken_services;
+                priv->broken_services = NULL;
+
+                reconnect_service (source);
+        }
+
+        return G_SOURCE_REMOVE;
+}
+
+static void
+check_unbreak_timer (GClueNMEASource *source)
+{
+        GClueNMEASourcePrivate *priv = source->priv;
+
+        if (priv->try_services || !priv->broken_services) {
+                if (priv->unbreak_timer) {
+                        g_debug ("Removing unnecessary NMEA unbreaking timer");
+
+                        g_source_remove (priv->unbreak_timer);
+                        priv->unbreak_timer = 0;
+                }
+
+                return;
+        }
+
+        if (priv->unbreak_timer) {
+                return;
+        }
+
+        g_debug ("Scheduling NMEA unbreaking timer");
+        priv->unbreak_timer = g_timeout_add_seconds (SERVICE_UNBREAK_TIME,
+                                                     on_service_unbreak_time,
+                                                     source);
+}
+
+static void
+service_lists_changed (GClueNMEASource *source)
+{
+        check_unbreak_timer (source);
+        reconnect_service (source);
+        refresh_accuracy_level (source);
+}
+
+static gboolean
+check_service_exists (GClueNMEASource *source,
+                      const char *name)
+{
+        GClueNMEASourcePrivate *priv = source->priv;
+        AvahiServiceInfo *service;
+        GList *item;
+        gboolean ret = FALSE;
+
+        /* only `name` is required here */
+        service = avahi_service_new (name,
+                                     NULL,
+                                     0,
+                                     GCLUE_ACCURACY_LEVEL_NONE);
+
+        item = g_list_find_custom (priv->try_services,
+                                   service,
+                                   compare_avahi_service_by_identifier);
+        if (item) {
+                ret = TRUE;
+        } else {
+                item = g_list_find_custom (priv->broken_services,
+                                           service,
+                                           compare_avahi_service_by_identifier);
+                if (item) {
+                        ret = TRUE;
+                }
+        }
+
+        g_clear_pointer (&service, avahi_service_free);
+
+        return ret;
 }
 
 static void
@@ -191,6 +344,7 @@ add_new_service (GClueNMEASource *source,
                  const char *name,
                  const char *host_name,
                  uint16_t port,
+                 gboolean is_socket,
                  AvahiStringList *txt)
 {
         GClueAccuracyLevel accuracy = GCLUE_ACCURACY_LEVEL_NONE;
@@ -201,10 +355,15 @@ add_new_service (GClueNMEASource *source,
         GEnumClass *enum_class;
         GEnumValue *enum_value;
 
-        if (port == 0) {
-	        accuracy = GCLUE_ACCURACY_LEVEL_EXACT;
+        if (check_service_exists (source, name)) {
+                g_debug ("NMEA service %s already exists", name);
+                return;
+        }
+
+        if (!txt) {
+                accuracy = GCLUE_ACCURACY_LEVEL_EXACT;
 
-	        goto CREATE_SERVICE;
+                goto CREATE_SERVICE;
         }
 
         node = avahi_string_list_find (txt, "accuracy");
@@ -242,43 +401,72 @@ add_new_service (GClueNMEASource *source,
 
 CREATE_SERVICE:
         service = avahi_service_new (name, host_name, port, accuracy);
+        service->is_socket = is_socket;
 
-        source->priv->all_services = g_list_insert_sorted
-                (source->priv->all_services,
+        source->priv->try_services = g_list_insert_sorted
+                (source->priv->try_services,
                  service,
                  compare_avahi_service_by_accuracy_n_time);
 
-        refresh_accuracy_level (source);
-        reconnect_service (source);
+        n_services = g_list_length (source->priv->try_services);
+        g_debug ("No. of _nmea-0183._tcp services %u", n_services);
 
-        n_services = g_list_length (source->priv->all_services);
+        service_lists_changed (source);
+}
 
-        g_debug ("No. of _nmea-0183._tcp services %u", n_services);
+static void
+add_new_service_avahi (GClueNMEASource *source,
+                       const char *name,
+                       const char *host_name,
+                       uint16_t port,
+                       AvahiStringList *txt)
+{
+        add_new_service (source, name, host_name, port, FALSE, txt);
 }
 
 static void
-remove_service (GClueNMEASource *source,
-                AvahiServiceInfo *service)
+add_new_service_socket (GClueNMEASource *source,
+                       const char *name,
+                       const char *socket_path)
 {
-        guint n_services = 0;
+        add_new_service (source, name, socket_path, 0, TRUE, NULL);
+}
 
-        avahi_service_free (service);
-        source->priv->all_services = g_list_remove
-                (source->priv->all_services, service);
+static void
+service_broken (GClueNMEASource *source)
+{
+        GClueNMEASourcePrivate *priv = source->priv;
+        AvahiServiceInfo *service = priv->active_service;
 
-        n_services = g_list_length (source->priv->all_services);
+        g_assert (service);
 
-        g_debug ("No. of _nmea-0183._tcp services %u",
-                 n_services);
+        disconnect_from_service (source);
 
-        refresh_accuracy_level (source);
-        reconnect_service (source);
+        priv->try_services = g_list_remove (priv->try_services,
+                                            service);
+        priv->broken_services = g_list_insert_sorted
+                (priv->broken_services,
+                 service,
+                compare_avahi_service_by_accuracy_n_time);
+
+        service_lists_changed (source);
+}
+
+static void
+remove_service_from_list (GList **list,
+                          GList *item)
+{
+        AvahiServiceInfo *service = item->data;
+
+        *list = g_list_delete_link (*list, item);
+        avahi_service_free (service);
 }
 
 static void
 remove_service_by_name (GClueNMEASource *source,
                         const char      *name)
 {
+        GClueNMEASourcePrivate *priv = source->priv;
         AvahiServiceInfo *service;
         GList *item;
 
@@ -288,15 +476,31 @@ remove_service_by_name (GClueNMEASource *source,
                                      0,
                                      GCLUE_ACCURACY_LEVEL_NONE);
 
-        item = g_list_find_custom (source->priv->all_services,
+        item = g_list_find_custom (priv->try_services,
                                    service,
                                    compare_avahi_service_by_identifier);
-        avahi_service_free (service);
+        if (item) {
+                if (item->data == priv->active_service) {
+                        g_debug ("Active NMEA service removed, disconnecting.");
+                        disconnect_from_service (source);
+                }
 
-        if (item == NULL)
-                return;
+                remove_service_from_list (&priv->try_services,
+                                          item);
+        } else {
+                item = g_list_find_custom (priv->broken_services,
+                                           service,
+                                           compare_avahi_service_by_identifier);
+                if (item) {
+                        g_assert (item->data != priv->active_service);
+                        remove_service_from_list (&priv->broken_services,
+                                                  item);
+                }
+        }
 
-        remove_service (source, item->data);
+        g_clear_pointer (&service, avahi_service_free);
+
+        service_lists_changed (source);
 }
 
 static void
@@ -337,15 +541,18 @@ resolve_callback (AvahiServiceResolver  *service_resolver,
         }
 
         case AVAHI_RESOLVER_FOUND:
-                g_debug ("Service %s:%u resolved",
+                g_debug ("Service '%s' of type '%s' in domain '%s' resolved to %s:%u",
+                         name,
+                         type,
+                         domain,
                          host_name,
-                         port);
+                         (unsigned int)port);
 
-                add_new_service (GCLUE_NMEA_SOURCE (user_data),
-                                 name,
-                                 host_name,
-                                 port,
-                                 txt);
+                add_new_service_avahi (GCLUE_NMEA_SOURCE (user_data),
+                                       name,
+                                       host_name,
+                                       port,
+                                       txt);
 
                 break;
         }
@@ -358,12 +565,8 @@ client_callback (AvahiClient     *avahi_client,
                  AvahiClientState state,
                  void            *user_data)
 {
-        GClueNMEASourcePrivate *priv = GCLUE_NMEA_SOURCE (user_data)->priv;
-
         g_return_if_fail (avahi_client != NULL);
 
-        priv->avahi_client = avahi_client;
-
         if (state == AVAHI_CLIENT_FAILURE) {
                 const char *errorstr = avahi_strerror
                         (avahi_client_errno (avahi_client));
@@ -448,53 +651,97 @@ browse_callback (AvahiServiceBrowser   *service_browser,
         }
 }
 
+#define NMEA_LINE_END "\r\n"
+#define NMEA_LINE_END_CTR (sizeof (NMEA_LINE_END) - 1)
+
+static void nmea_skip_delim (GBufferedInputStream *stream,
+                             GCancellable *cancellable)
+{
+        const char *buf;
+        gsize buf_size;
+        size_t delim_skip;
+        g_autoptr(GError) error = NULL;
+
+        buf = (const char *) g_buffered_input_stream_peek_buffer (stream,
+                                                                  &buf_size);
+
+        delim_skip = strnspn (buf, NMEA_LINE_END, buf_size);
+        for (size_t ctr = 0; ctr < delim_skip; ctr++) {
+                if (g_buffered_input_stream_read_byte (stream, cancellable, &error) < 0) {
+                        if (error && !g_error_matches (error, G_IO_ERROR,
+                                                       G_IO_ERROR_CANCELLED)) {
+                                g_warning ("Failed to skip %zu / %zu NMEA delimiter: %s",
+                                           ctr, delim_skip, error->message);
+                        }
+                        break;
+                }
+        }
+}
+
+static gboolean nmea_check_delim (GBufferedInputStream *stream)
+{
+        const char *buf;
+        gsize buf_size;
+
+        buf = (const char *) g_buffered_input_stream_peek_buffer (stream,
+                                                                  &buf_size);
+
+        return strnpbrk (buf, NMEA_LINE_END, buf_size) != NULL;
+}
+
 #define NMEA_STR_LEN 128
 static void
 on_read_nmea_sentence (GObject      *object,
                        GAsyncResult *result,
                        gpointer      user_data)
 {
-        GClueNMEASource *source = GCLUE_NMEA_SOURCE (user_data);
+        GClueNMEASource *source = NULL;
         GDataInputStream *data_input_stream = G_DATA_INPUT_STREAM (object);
-        GError *error = NULL;
+        g_autoptr(GError) error = NULL;
         GClueLocation *prev_location;
         g_autoptr(GClueLocation) location = NULL;
         gsize data_size = 0 ;
-        char *message;
+        g_autofree char *message = NULL;
         gint i;
-        static const gchar *sentences[3] = { 0 };
-        static gchar gga[NMEA_STR_LEN] = { 0 };
-        static gchar rmc[NMEA_STR_LEN] = { 0 };
+        const gchar *sentences[3];
+        gchar gga[NMEA_STR_LEN];
+        gchar rmc[NMEA_STR_LEN];
 
-
-        message = g_data_input_stream_read_line_finish (data_input_stream,
+        message = g_data_input_stream_read_upto_finish (data_input_stream,
                                                         result,
                                                         &data_size,
                                                         &error);
 
+        gga[0] = '\0';
+        rmc[0] = '\0';
+
         do {
+                if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+                        return;
+
+                if (!source)
+                        source = GCLUE_NMEA_SOURCE (user_data);
+
                 if (message == NULL) {
                         if (error != NULL) {
-                                if (error->code == G_IO_ERROR_CLOSED)
-                                        g_debug ("Socket closed.");
-                                else if (error->code != G_IO_ERROR_CANCELLED)
+                                if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CLOSED)) {
+                                        g_debug ("NMEA socket closed.");
+                                } else {
                                         g_warning ("Error when receiving message: %s",
                                                    error->message);
-                                g_error_free (error);
+                                }
+                                service_broken (source);
+                                return;
                         } else {
-                                g_debug ("Nothing to read");
-                        }
-                        g_object_unref (data_input_stream);
-
-                        if (source->priv->active_service != NULL)
-                                /* In case service did not advertise it exiting
-                                 * or we failed to receive it's notification.
+                                g_debug ("NMEA empty read");
+                                /* GLib has a bug where g_data_input_stream_read_upto_finish
+                                 * returns NULL when reading a line with only stop chars.
+                                 * Convert this NULL to a zero-length message. See:
+                                 * https://gitlab.gnome.org/GNOME/glib/-/issues/655
                                  */
-                                remove_service (source, source->priv->active_service);
+                                message = g_strdup ("");
+                        }
 
-                        gga[0] = '\0';
-                        rmc[0] = '\0';
-                        return;
                 }
                 g_debug ("Network source sent: \"%s\"", message);
 
@@ -502,16 +749,17 @@ on_read_nmea_sentence (GObject      *object,
                         g_strlcpy (gga, message, NMEA_STR_LEN);
                 } else if (gclue_nmea_type_is (message, "RMC")) {
                         g_strlcpy (rmc, message, NMEA_STR_LEN);
-                } else {
-                        g_debug ("Ignoring NMEA sentence, as it's neither GGA or RMC: %s", message);
                 }
 
-                message = (char *) g_buffered_input_stream_peek_buffer
-                        (G_BUFFERED_INPUT_STREAM (data_input_stream),
-                         &data_size);
-                if (g_strstr_len (message, data_size, "\n")) {
-                    message = g_data_input_stream_read_line
-                            (data_input_stream, &data_size, NULL, &error);
+                nmea_skip_delim (G_BUFFERED_INPUT_STREAM (data_input_stream),
+                                 source->priv->cancellable);
+
+                if (nmea_check_delim (G_BUFFERED_INPUT_STREAM (data_input_stream))) {
+                    g_clear_pointer (&message, g_free);
+                    message = g_data_input_stream_read_upto
+                            (data_input_stream,
+                             NMEA_LINE_END, NMEA_LINE_END_CTR,
+                             &data_size, NULL, &error);
                 } else {
                     break;
                 }
@@ -524,25 +772,20 @@ on_read_nmea_sentence (GObject      *object,
                 sentences[i++] = rmc;
         sentences[i] = NULL;
 
-        prev_location = gclue_location_source_get_location
-                (GCLUE_LOCATION_SOURCE (source));
-        location = gclue_location_create_from_nmeas (sentences,
-                                                     prev_location,
-                                                     &error);
-
-        if (error != NULL) {
-                g_warning ("Error: %s", error->message);
-                g_clear_error (&error);
-        } else {
-                gclue_location_source_set_location
-                        (GCLUE_LOCATION_SOURCE (source), location);
+        if (i > 0) {
+                prev_location = gclue_location_source_get_location
+                        (GCLUE_LOCATION_SOURCE (source));
+                location = gclue_location_create_from_nmeas (sentences,
+                                                             prev_location);
+                if (location) {
+                        gclue_location_source_set_location
+                                (GCLUE_LOCATION_SOURCE (source), location);
+                }
         }
 
-        gga[0] = '\0';
-        rmc[0] = '\0';
-        sentences[0] = NULL;
-
-        g_data_input_stream_read_line_async (data_input_stream,
+        g_data_input_stream_read_upto_async (data_input_stream,
+                                             NMEA_LINE_END,
+                                             NMEA_LINE_END_CTR,
                                              G_PRIORITY_DEFAULT,
                                              source->priv->cancellable,
                                              on_read_nmea_sentence,
@@ -554,30 +797,41 @@ on_connection_to_location_server (GObject      *object,
                                   GAsyncResult *result,
                                   gpointer      user_data)
 {
-        GClueNMEASource *source = GCLUE_NMEA_SOURCE (user_data);
         GSocketClient *client = G_SOCKET_CLIENT (object);
-        GError *error = NULL;
-        GDataInputStream *data_input_stream;
-        GInputStream *input_stream;
+        GClueNMEASource *source;
+        g_autoptr(GSocketConnection) connection = NULL;
+        g_autoptr(GError) error = NULL;
 
-        source->priv->connection = g_socket_client_connect_to_host_finish
+        connection = g_socket_client_connect_to_host_finish
                 (client,
                  result,
                  &error);
 
-        if (error != NULL) {
-                if (error->code != G_IO_ERROR_CANCELLED)
-                        g_warning ("Failed to connect to NMEA service: %s", error->message);
-                g_clear_error (&error);
+        if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
+                return;
+        }
 
+        source = GCLUE_NMEA_SOURCE (user_data);
+
+        if (error != NULL) {
+                g_warning ("Failed to connect to NMEA service: %s", error->message);
+                service_broken (source);
                 return;
         }
 
-        input_stream = g_io_stream_get_input_stream
-                (G_IO_STREAM (source->priv->connection));
-        data_input_stream = g_data_input_stream_new (input_stream);
+        g_assert (connection);
+        g_debug ("NMEA service connected.");
 
-        g_data_input_stream_read_line_async (data_input_stream,
+        g_assert (!source->priv->connection);
+        source->priv->connection = g_steal_pointer (&connection);
+
+        g_assert (!source->priv->input_stream);
+        source->priv->input_stream = g_data_input_stream_new
+                (g_io_stream_get_input_stream (G_IO_STREAM (source->priv->connection)));
+
+        g_data_input_stream_read_upto_async (source->priv->input_stream,
+                                             NMEA_LINE_END,
+                                             NMEA_LINE_END_CTR,
                                              G_PRIORITY_DEFAULT,
                                              source->priv->cancellable,
                                              on_read_nmea_sentence,
@@ -585,77 +839,126 @@ on_connection_to_location_server (GObject      *object,
 }
 
 static void
-connect_to_service (GClueNMEASource *source)
+try_connect_to_service (GClueNMEASource *source)
 {
         GClueNMEASourcePrivate *priv = source->priv;
-        GSocketAddress *addr;
-        GSocketConnectable *connectable;
 
-        if (priv->all_services == NULL)
+        if (!gclue_location_source_get_active (GCLUE_LOCATION_SOURCE (source))) {
+                g_warn_if_fail (!priv->active_service);
+
+                return;
+        }
+
+        if (priv->active_service)
+                return;
+
+        if (priv->try_services == NULL)
                 return;
 
+        g_assert (!priv->cancellable);
+        priv->cancellable = g_cancellable_new ();
+
+        g_assert (!priv->client);
         priv->client = g_socket_client_new ();
-        g_cancellable_reset (priv->cancellable);
 
         /* The service with the highest accuracy will be stored in the beginning
          * of the list.
          */
-        priv->active_service = (AvahiServiceInfo *) priv->all_services->data;
-
-        if ( priv->active_service->port != 0 )
-		g_socket_client_connect_to_host_async
-			(priv->client,
-			 priv->active_service->host_name,
-			 priv->active_service->port,
-			 priv->cancellable,
-			 on_connection_to_location_server,
-			 source);
-        else {
-		addr = g_unix_socket_address_new(priv->active_service->host_name);
-		connectable = G_SOCKET_CONNECTABLE (addr);
-		g_socket_client_connect_async (priv->client,
-                               connectable,
+        priv->active_service = (AvahiServiceInfo *) priv->try_services->data;
+
+        g_debug ("Trying to connect to NMEA %sservice %s:%u.",
+                 priv->active_service->is_socket ? "socket " : "",
+                 priv->active_service->host_name,
+                 (unsigned int) priv->active_service->port);
+
+        if (!priv->active_service->is_socket) {
+                g_socket_client_connect_to_host_async
+                        (priv->client,
+                         priv->active_service->host_name,
+                         priv->active_service->port,
+                         priv->cancellable,
+                         on_connection_to_location_server,
+                         source);
+        } else {
+                g_autoptr(GSocketAddress) addr = NULL;
+
+                addr = g_unix_socket_address_new (priv->active_service->host_name);
+                g_socket_client_connect_async (priv->client,
+                               G_SOCKET_CONNECTABLE (addr),
                                priv->cancellable,
                                on_connection_to_location_server,
                                source);
         }
 }
 
-static void
-disconnect_from_service (GClueNMEASource *source)
+static gboolean
+remove_avahi_services_from_list (GClueNMEASource *source, GList **list)
 {
         GClueNMEASourcePrivate *priv = source->priv;
+        gboolean removed_active = FALSE;
+        GList *l = *list;
+
+        while (l != NULL) {
+                GList *next = l->next;
+                AvahiServiceInfo *service = l->data;
+
+                if (!service->is_socket) {
+                        if (service == priv->active_service) {
+                                g_debug ("Active NMEA service was Avahi-provided, disconnecting.");
+                                disconnect_from_service (source);
+                                removed_active = TRUE;
+                        }
 
-        g_cancellable_cancel (priv->cancellable);
+                        remove_service_from_list (list, l);
+                }
 
-        if (priv->connection != NULL) {
-                GError *error = NULL;
+                l = next;
+        }
+
+        return removed_active;
+}
+
+static void
+disconnect_avahi_client (GClueNMEASource *source)
+{
+        GClueNMEASourcePrivate *priv = source->priv;
 
-                g_io_stream_close (G_IO_STREAM (priv->connection),
-                                   NULL,
-                                   &error);
-                if (error != NULL)
-                        g_warning ("Error in closing socket connection: %s", error->message);
+        remove_avahi_services_from_list (source, &priv->try_services);
+        if (remove_avahi_services_from_list (source, &priv->broken_services)) {
+                g_warn_if_reached ();
         }
 
-        g_clear_object (&priv->connection);
-        g_clear_object (&priv->client);
-        priv->active_service = NULL;
+        g_clear_pointer (&priv->avahi_client, avahi_client_free);
+
+        service_lists_changed (source);
 }
 
 static void
 gclue_nmea_source_finalize (GObject *gnmea)
 {
-        GClueNMEASourcePrivate *priv = GCLUE_NMEA_SOURCE (gnmea)->priv;
+        GClueNMEASource *source = GCLUE_NMEA_SOURCE (gnmea);
+        GClueNMEASourcePrivate *priv = source->priv;
 
         G_OBJECT_CLASS (gclue_nmea_source_parent_class)->finalize (gnmea);
 
-        g_clear_object (&priv->connection);
-        g_clear_object (&priv->client);
-        g_clear_object (&priv->cancellable);
-        if (priv->avahi_client)
-                avahi_client_free (priv->avahi_client);
-        g_list_free_full (priv->all_services,
+        disconnect_avahi_client (source);
+        disconnect_from_service (source);
+
+        if (priv->accuracy_refresh_source) {
+                g_source_remove (priv->accuracy_refresh_source);
+                priv->accuracy_refresh_source = 0;
+        }
+
+        if (priv->unbreak_timer) {
+                g_source_remove (priv->unbreak_timer);
+                priv->unbreak_timer = 0;
+        }
+
+        g_clear_pointer (&priv->glib_poll, avahi_glib_poll_free);
+
+        g_list_free_full (g_steal_pointer (&priv->try_services),
+                          avahi_service_free);
+        g_list_free_full (g_steal_pointer (&priv->broken_services),
                           avahi_service_free);
 }
 
@@ -672,41 +975,33 @@ gclue_nmea_source_class_init (GClueNMEASourceClass *klass)
 }
 
 static void
-gclue_nmea_source_init (GClueNMEASource *source)
+try_connect_avahi_client (GClueNMEASource *source)
 {
-        GClueNMEASourcePrivate *priv;
         AvahiServiceBrowser *service_browser;
+        GClueNMEASourcePrivate *priv = source->priv;
         const AvahiPoll *poll_api;
-        AvahiGLibPoll *glib_poll;
-        const char *nmea_socket;
-        GClueConfig *config;
         int error;
 
-        source->priv = gclue_nmea_source_get_instance_private (source);
-        priv = source->priv;
-
-        glib_poll = avahi_glib_poll_new (NULL, G_PRIORITY_DEFAULT);
-        poll_api = avahi_glib_poll_get (glib_poll);
-
-        priv->cancellable = g_cancellable_new ();
+        if (priv->avahi_client) {
+                AvahiClientState avahi_state;
 
-        config = gclue_config_get_singleton ();
+                avahi_state = avahi_client_get_state (priv->avahi_client);
+                if (avahi_state != AVAHI_CLIENT_FAILURE) {
+                        return;
+                }
 
-        nmea_socket = gclue_config_get_nmea_socket (config);
-        if (nmea_socket != NULL) {
-                add_new_service (source,
-                                 "nmea-socket",
-                                 nmea_socket,
-                                 0,
-                                 NULL);
+                g_debug ("Avahi client in failure state, trying to reinit.");
+                disconnect_avahi_client (source);
         }
 
-        avahi_client_new (poll_api,
-                          0,
-                          client_callback,
-                          source,
-                          &error);
+        g_assert (priv->glib_poll);
+        poll_api = avahi_glib_poll_get (priv->glib_poll);
 
+        priv->avahi_client = avahi_client_new (poll_api,
+                                               0,
+                                               client_callback,
+                                               source,
+                                               &error);
         if (priv->avahi_client == NULL) {
                 g_warning ("Failed to connect to avahi service: %s",
                            avahi_strerror (error));
@@ -722,15 +1017,43 @@ gclue_nmea_source_init (GClueNMEASource *source)
                  0,
                  browse_callback,
                  source);
-
-
         if (service_browser == NULL) {
                 const char *errorstr;
 
                 error = avahi_client_errno (priv->avahi_client);
                 errorstr = avahi_strerror (error);
                 g_warning ("Failed to browse avahi services: %s", errorstr);
+                goto fail_client;
         }
+
+        return;
+
+fail_client:
+        disconnect_avahi_client (source);
+}
+
+static void
+gclue_nmea_source_init (GClueNMEASource *source)
+{
+        GClueNMEASourcePrivate *priv;
+        const char *nmea_socket;
+        GClueConfig *config;
+
+        source->priv = gclue_nmea_source_get_instance_private (source);
+        priv = source->priv;
+
+        priv->glib_poll = avahi_glib_poll_new (NULL, G_PRIORITY_DEFAULT);
+
+        config = gclue_config_get_singleton ();
+
+        nmea_socket = gclue_config_get_nmea_socket (config);
+        if (nmea_socket != NULL) {
+                add_new_service_socket (source,
+                                        "nmea-socket",
+                                        nmea_socket);
+        }
+
+        try_connect_avahi_client (source);
 }
 
 /**
@@ -747,11 +1070,15 @@ gclue_nmea_source_get_singleton (void)
         static GClueNMEASource *source = NULL;
 
         if (source == NULL) {
-                source = g_object_new (GCLUE_TYPE_NMEA_SOURCE, NULL);
+                source = g_object_new (GCLUE_TYPE_NMEA_SOURCE,
+                                       "priority-source", TRUE,
+                                       NULL);
                 g_object_add_weak_pointer (G_OBJECT (source),
                                            (gpointer) &source);
-        } else
+        } else {
                 g_object_ref (source);
+                try_connect_avahi_client (source);
+        }
 
         return source;
 }
@@ -767,10 +1094,11 @@ gclue_nmea_source_start (GClueLocationSource *source)
 
         base_class = GCLUE_LOCATION_SOURCE_CLASS (gclue_nmea_source_parent_class);
         base_result = base_class->start (source);
-        if (base_result != GCLUE_LOCATION_SOURCE_START_RESULT_OK)
+        if (base_result == GCLUE_LOCATION_SOURCE_START_RESULT_FAILED)
                 return base_result;
 
-        connect_to_service (GCLUE_NMEA_SOURCE (source));
+        try_connect_avahi_client (GCLUE_NMEA_SOURCE (source));
+        reconnect_service (GCLUE_NMEA_SOURCE (source));
 
         return base_result;
 }
diff --git a/src/gclue-nmea-utils.c b/src/gclue-nmea-utils.c
index b38196d..ed5aa18 100644
--- a/src/gclue-nmea-utils.c
+++ b/src/gclue-nmea-utils.c
@@ -36,3 +36,39 @@ gclue_nmea_type_is (const char *msg, const char *nmeatype)
                 g_str_has_prefix (msg+3, nmeatype);
 }
 
+/**
+ * gclue_nmea_timestamp_to_timespan
+ * @timestamp: NMEA timestamp string
+ *
+ * Parse the NMEA timestamp string, which is a field in NMEA sentences
+ * like GGA and RMC.
+ *
+ * Returns: a GTimeSpan (gint64) value of microseconds since midnight,
+ * or -1, if reading fails.
+ */
+GTimeSpan
+gclue_nmea_timestamp_to_timespan (const gchar *timestamp)
+{
+        gint its, hours, minutes;
+        gdouble ts, seconds_f;
+        gchar *endptr;
+
+        if (!timestamp || !*timestamp)
+            return -1;
+
+        ts = g_ascii_strtod (timestamp, &endptr);
+        if (endptr != timestamp + g_utf8_strlen(timestamp, 12) ||
+            ts < 0.0 ||
+            ts >= 235960.0)
+                return -1;
+
+        its = (gint) ts;  /* Truncate towards zero */
+        hours = its / 10000;
+        minutes = (its - 10000 * hours) / 100;
+        seconds_f = ts - 10000 * hours - 100 * minutes;  /* Seconds plus fraction */
+
+        return (GTimeSpan) G_USEC_PER_SEC * (3600 * hours +
+                                               60 * minutes +
+                                                    seconds_f);
+}
+
diff --git a/src/gclue-nmea-utils.h b/src/gclue-nmea-utils.h
index 618dda7..fd51454 100644
--- a/src/gclue-nmea-utils.h
+++ b/src/gclue-nmea-utils.h
@@ -24,6 +24,7 @@
 G_BEGIN_DECLS
 
 gboolean         gclue_nmea_type_is              (const char *msg, const char *nmeatype);
+GTimeSpan        gclue_nmea_timestamp_to_timespan (const gchar *timestamp);
 
 G_END_DECLS
 
diff --git a/src/gclue-service-client.c b/src/gclue-service-client.c
index 8f0b353..77a5b82 100644
--- a/src/gclue-service-client.c
+++ b/src/gclue-service-client.c
@@ -86,13 +86,13 @@ static char *
 next_location_path (GClueServiceClient *client)
 {
         GClueServiceClientPrivate *priv = client->priv;
-        char *path, *index_str;
+        g_autofree char *index_str = NULL;
+        g_autofree char *path = NULL;
 
         index_str = g_strdup_printf ("%u", (priv->locations_updated)++),
         path = g_strjoin ("/", priv->path, "Location", index_str, NULL);
-        g_free (index_str);
 
-        return path;
+        return g_steal_pointer (&path);
 }
 
 /* We don't use the gdbus-codegen provided gclue_client_emit_location_updated()
@@ -201,9 +201,9 @@ on_locator_location_changed (GObject    *gobject,
         GClueServiceClientPrivate *priv = client->priv;
         GClueLocationSource *locator = GCLUE_LOCATION_SOURCE (gobject);
         GClueLocation *new_location;
-        char *path = NULL;
+        g_autofree char *path = NULL;
         const char *prev_path;
-        GError *error = NULL;
+        g_autoptr(GError) error = NULL;
 
         new_location = gclue_location_source_get_location (locator);
         if (new_location == NULL)
@@ -245,13 +245,10 @@ on_locator_location_changed (GObject    *gobject,
         if (!emit_location_updated (client, prev_path, path, &error))
                 goto error_out;
 
-        goto out;
+        return;
 
 error_out:
         g_warning ("Failed to update location info: %s", error->message);
-        g_error_free (error);
-out:
-        g_free (path);
 }
 
 static void
@@ -262,10 +259,10 @@ start_client (GClueServiceClient *client, GClueAccuracyLevel accuracy_level)
         gclue_dbus_client_set_active (GCLUE_DBUS_CLIENT (client), TRUE);
         priv->locator = gclue_locator_new (accuracy_level);
         gclue_locator_set_time_threshold (priv->locator, priv->time_threshold);
-        g_signal_connect (priv->locator,
-                          "notify::location",
-                          G_CALLBACK (on_locator_location_changed),
-                          client);
+        g_signal_connect_object (priv->locator,
+                                 "notify::location",
+                                 G_CALLBACK (on_locator_location_changed),
+                                 client, 0);
 
         gclue_location_source_start (GCLUE_LOCATION_SOURCE (priv->locator));
 }
@@ -321,7 +318,7 @@ on_agent_props_changed (GDBusProxy *agent_proxy,
         GVariantIter *iter;
         GVariant *value;
         gchar *key;
-        
+
         if (g_variant_n_children (changed_properties) <= 0)
                 return;
 
@@ -375,7 +372,7 @@ start_data_free (StartData *data)
 {
         g_object_unref (data->client);
         g_object_unref (data->invocation);
-        g_free(data->desktop_id);
+        g_free (data->desktop_id);
         g_slice_free (StartData, data);
 }
 
diff --git a/src/gclue-service-manager.c b/src/gclue-service-manager.c
index ba06c35..56322de 100644
--- a/src/gclue-service-manager.c
+++ b/src/gclue-service-manager.c
@@ -21,7 +21,9 @@
  */
 
 #include <glib/gi18n.h>
+#include <glib-unix.h>
 #include <config.h>
+#include <signal.h>
 
 #include "gclue-service-manager.h"
 #include "gclue-service-client.h"
@@ -45,6 +47,7 @@ struct _GClueServiceManagerPrivate
 
         guint last_client_id;
         guint num_clients;
+        guint unix_signal_source;
 
         GClueLocator *locator;
 };
@@ -77,6 +80,37 @@ typedef struct
 
 } OnClientInfoNewReadyData;
 
+static gboolean
+log_client_list (gpointer user_data)
+{
+        GClueServiceManager *manager = GCLUE_SERVICE_MANAGER (user_data);
+        GList *l;
+
+        g_message ("SIGUSR1 received, printing client list:");
+        if (!manager->priv->clients) {
+                g_message ("    (No clients)");
+                return G_SOURCE_CONTINUE;
+        }
+        g_message ("    System  Active  UID     Id");
+        for (l = manager->priv->clients; l != NULL; l = l->next) {
+                GClueDBusClient *dbus_client = GCLUE_DBUS_CLIENT (l->data);
+                GClueClientInfo *client_info =
+                        gclue_service_client_get_client_info (GCLUE_SERVICE_CLIENT (l->data));
+                GClueConfig *config;
+                guint32 uid;
+                const char *system, *active, *id;
+                const char *y = "x";
+                const char *n = "-";
+                id = gclue_dbus_client_get_desktop_id (dbus_client);
+                uid = gclue_client_info_get_user_id (client_info);
+                config = gclue_config_get_singleton ();
+                system = gclue_config_is_system_component (config, id) ? y : n;
+                active = gclue_dbus_client_get_active (dbus_client) ? y : n;
+                g_message ("    %s       %s       %-7d %s", system, active, uid, id);
+        }
+        return G_SOURCE_CONTINUE;
+}
+
 static void
 on_client_info_new_ready_data_free (gpointer data)
 {
@@ -180,8 +214,8 @@ complete_get_client (OnClientInfoNewReadyData *data)
         GClueServiceClient *client;
         GClueClientInfo *info = data->client_info;
         GClueAgent *agent_proxy = NULL;
-        GError *error = NULL;
-        char *path;
+        g_autoptr(GError) error = NULL;
+        g_autofree char *path = NULL;
         guint32 user_id;
 
         /* Disconnect on_peer_vanished_before_completion, if it's there */
@@ -228,15 +262,15 @@ complete_get_client (OnClientInfoNewReadyData *data)
         }
         g_debug ("Number of connected clients: %u", priv->num_clients);
 
-        g_signal_connect (client,
-                          "notify::active",
-                          G_CALLBACK (on_client_notify_active),
-                          data->manager);
+        g_signal_connect_object (client,
+                                 "notify::active",
+                                 G_CALLBACK (on_client_notify_active),
+                                 data->manager, 0);
 
-        g_signal_connect (info,
-                          "peer-vanished",
-                          G_CALLBACK (on_peer_vanished),
-                          data->manager);
+        g_signal_connect_object (info,
+                                 "peer-vanished",
+                                 G_CALLBACK (on_peer_vanished),
+                                 data->manager, 0);
 
 client_created:
         if (data->reuse_client)
@@ -255,10 +289,8 @@ error_out:
                                                        G_DBUS_ERROR_FAILED,
                                                        error->message);
 out:
-        g_clear_error (&error);
         g_clear_object (&info);
         on_client_info_new_ready_data_free (data);
-        g_free (path);
 
         return FALSE;
 }
@@ -296,7 +328,7 @@ on_client_info_new_ready (GObject      *source_object,
         GClueServiceManagerPrivate *priv = GCLUE_SERVICE_MANAGER (data->manager)->priv;
         GClueClientInfo *info = NULL;
         GClueAgent *agent_proxy;
-        GError *error = NULL;
+        g_autoptr(GError) error = NULL;
         guint32 user_id;
 
         info = gclue_client_info_new_finish (res, &error);
@@ -305,7 +337,6 @@ on_client_info_new_ready (GObject      *source_object,
                                                                G_DBUS_ERROR,
                                                                G_DBUS_ERROR_FAILED,
                                                                error->message);
-                g_error_free (error);
                 on_client_info_new_ready_data_free (data);
 
                 return;
@@ -434,6 +465,7 @@ static void
 add_agent_data_free (AddAgentData *data)
 {
         g_clear_pointer (&data->desktop_id, g_free);
+        g_clear_object (&data->manager);
         g_slice_free (AddAgentData, data);
 }
 
@@ -459,7 +491,7 @@ on_agent_proxy_ready (GObject      *source_object,
         GClueServiceManagerPrivate *priv = GCLUE_SERVICE_MANAGER (data->manager)->priv;
         guint32 user_id;
         GClueAgent *agent;
-        GError *error = NULL;
+        g_autoptr(GError) error = NULL;
         GList *l;
 
         agent = gclue_agent_proxy_new_for_bus_finish (res, &error);
@@ -470,10 +502,10 @@ on_agent_proxy_ready (GObject      *source_object,
         g_debug ("New agent for user ID '%u'", user_id);
         g_hash_table_replace (priv->agents, GINT_TO_POINTER (user_id), agent);
 
-        g_signal_connect (data->info,
-                          "peer-vanished",
-                          G_CALLBACK (on_agent_vanished),
-                          data->manager);
+        g_signal_connect_object (data->info,
+                                 "peer-vanished",
+                                 G_CALLBACK (on_agent_vanished),
+                                 data->manager, 0);
 
         gclue_dbus_manager_complete_add_agent (data->manager, data->invocation);
 
@@ -498,7 +530,6 @@ error_out:
                                                        G_DBUS_ERROR_FAILED,
                                                        error->message);
 out:
-        g_clear_error (&error);
         add_agent_data_free (data);
 }
 
@@ -510,7 +541,7 @@ on_agent_info_new_ready (GObject      *source_object,
                          gpointer      user_data)
 {
         AddAgentData *data = (AddAgentData *) user_data;
-        GError *error = NULL;
+        g_autoptr(GError) error = NULL;
         GClueConfig *config;
         const char *xdg_id;
 
@@ -520,7 +551,6 @@ on_agent_info_new_ready (GObject      *source_object,
                                                                G_DBUS_ERROR,
                                                                G_DBUS_ERROR_FAILED,
                                                                error->message);
-                g_error_free (error);
                 add_agent_data_free (data);
 
                 return;
@@ -564,7 +594,7 @@ gclue_service_manager_handle_add_agent (GClueDBusManager      *manager,
         peer = g_dbus_method_invocation_get_sender (invocation);
 
         data = g_slice_new0 (AddAgentData);
-        data->manager = manager;
+        data->manager = g_object_ref (manager);
         data->invocation = invocation;
         data->desktop_id = g_strdup (id);
         gclue_client_info_new_async (peer,
@@ -592,6 +622,7 @@ gclue_service_manager_finalize (GObject *object)
                                    on_client_info_new_ready_data_free);
                 priv->clients_waiting_agent = NULL;
         }
+        g_clear_handle_id (&priv->unix_signal_source, g_source_remove);
 
         /* Chain up to the parent class */
         G_OBJECT_CLASS (gclue_service_manager_parent_class)->finalize (object);
@@ -654,18 +685,20 @@ on_avail_accuracy_level_changed (GObject    *object,
 static void
 gclue_service_manager_constructed (GObject *object)
 {
-        GClueServiceManagerPrivate *priv = GCLUE_SERVICE_MANAGER (object)->priv;
+        GClueServiceManager *manager = GCLUE_SERVICE_MANAGER (object);
+        GClueServiceManagerPrivate *priv = manager->priv;
 
         G_OBJECT_CLASS (gclue_service_manager_parent_class)->constructed (object);
 
         priv->locator = gclue_locator_new (GCLUE_ACCURACY_LEVEL_EXACT);
-        g_signal_connect (G_OBJECT (priv->locator),
-                          "notify::available-accuracy-level",
-                          G_CALLBACK (on_avail_accuracy_level_changed),
-                          object);
+        g_signal_connect_object (G_OBJECT (priv->locator),
+                                 "notify::available-accuracy-level",
+                                 G_CALLBACK (on_avail_accuracy_level_changed),
+                                 object, 0);
         on_avail_accuracy_level_changed (G_OBJECT (priv->locator),
                                          NULL,
                                          object);
+        priv->unix_signal_source = g_unix_signal_add (SIGUSR1, log_client_list, manager);
 }
 
 static void
diff --git a/src/gclue-static-source.c b/src/gclue-static-source.c
new file mode 100644
index 0000000..1c35cea
--- /dev/null
+++ b/src/gclue-static-source.c
@@ -0,0 +1,491 @@
+/* vim: set et ts=8 sw=8: */
+/*
+ * Copyright © 2022,2023 Oracle and/or its affiliates.
+ *
+ * Geoclue is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free
+ * Software Foundation; either version 2 of the License, or (at your option)
+ * any later version.
+ *
+ * Geoclue is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with Geoclue; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ */
+
+#include <string.h>
+#include <gio/gio.h>
+#include "gclue-location.h"
+#include "gclue-static-source.h"
+#include "config.h"
+#include "gclue-enum-types.h"
+
+#define GEO_FILE_NAME "geolocation"
+#define GEO_FILE_PATH SYSCONFDIR "/" GEO_FILE_NAME
+
+/* Rate limit of geolocation file monitoring.
+ * In milliseconds.
+ */
+#define GEO_FILE_MONITOR_RATE_LIMIT 2500
+
+struct _GClueStaticSource {
+        /* <private> */
+        GClueLocationSource parent_instance;
+};
+
+typedef struct {
+        GClueLocation *location;
+        guint location_set_timer;
+
+        GFileMonitor *monitor;
+
+        GCancellable *cancellable;
+        gboolean file_open_quiet;
+        GFileInputStream *file_stream;
+        GDataInputStream *data_stream;
+        enum { L_LAT = 0, L_LON, L_ALT, L_ACCURACY } file_line;
+        gdouble latitude;
+        gdouble longitude;
+        gdouble altitude;
+} GClueStaticSourcePrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (GClueStaticSource,
+                            gclue_static_source,
+                            GCLUE_TYPE_LOCATION_SOURCE)
+
+static void
+file_read_next_line (GClueStaticSource *source);
+
+static GClueStaticSourcePrivate *
+get_priv (GClueStaticSource *source)
+{
+        return gclue_static_source_get_instance_private (source);
+}
+
+static void
+update_accuracy (GClueStaticSource *source)
+{
+        GClueStaticSourcePrivate *priv = get_priv (source);
+        GClueAccuracyLevel level_old, level_new;
+
+        if (!priv->location) {
+                level_new = GCLUE_ACCURACY_LEVEL_NONE;
+        } else {
+                gboolean scramble_location;
+
+                g_object_get (G_OBJECT(source), "scramble-location",
+                              &scramble_location, NULL);
+                if (scramble_location) {
+                        level_new = GCLUE_ACCURACY_LEVEL_CITY;
+                } else {
+                        level_new = GCLUE_ACCURACY_LEVEL_EXACT;
+                }
+        }
+
+        level_old = gclue_location_source_get_available_accuracy_level
+                (GCLUE_LOCATION_SOURCE (source));
+        if (level_new == level_old)
+                return;
+
+        g_debug ("Available accuracy level from %s: %u",
+                 G_OBJECT_TYPE_NAME (source), level_new);
+        g_object_set (G_OBJECT (source),
+                      "available-accuracy-level", level_new,
+                      NULL);
+}
+
+
+static gboolean
+on_location_set_timer (gpointer user_data)
+{
+        GClueStaticSource *source = GCLUE_STATIC_SOURCE (user_data);
+        GClueStaticSourcePrivate *priv = get_priv (source);
+        g_autoptr(GClueLocation) prev_location = NULL;
+
+        priv->location_set_timer = 0;
+
+        g_assert (priv->location);
+        prev_location = g_steal_pointer (&priv->location);
+        priv->location = gclue_location_duplicate_fresh (prev_location);
+        gclue_location_source_set_location
+                (GCLUE_LOCATION_SOURCE (source), priv->location);
+
+        return G_SOURCE_REMOVE;
+}
+
+static void
+location_set_refresh_timer (GClueStaticSource *source)
+{
+        GClueStaticSourcePrivate *priv = get_priv (source);
+
+        if (!priv->location) {
+                if (priv->location_set_timer) {
+                        g_debug ("Removing static location set timer due to no location");
+                        g_clear_handle_id (&priv->location_set_timer,
+                                           g_source_remove);
+                }
+
+                return;
+        }
+
+        if (priv->location_set_timer) {
+                return;
+        }
+
+        g_debug ("Scheduling static location set timer");
+        priv->location_set_timer = g_idle_add (on_location_set_timer,
+                                               source);
+}
+
+static void
+location_updated (GClueStaticSource *source)
+{
+        /* Update accuracy first so locators can connect or disconnect
+         * from our source accordingly before getting the new location.
+         */
+        update_accuracy (source);
+
+        location_set_refresh_timer (source);
+}
+
+static void
+close_file (GClueStaticSource *source)
+{
+        GClueStaticSourcePrivate *priv = get_priv (source);
+
+        if (!priv->cancellable)
+                return;
+
+        g_cancellable_cancel (priv->cancellable);
+
+        g_clear_object (&priv->data_stream);
+        g_clear_object (&priv->file_stream);
+        g_clear_object (&priv->cancellable);
+}
+
+static void
+close_file_clear_location (GClueStaticSource *source)
+{
+        GClueStaticSourcePrivate *priv = get_priv (source);
+
+        close_file (source);
+
+        if (!priv->location)
+                return;
+
+        g_debug ("Static source clearing location");
+        g_clear_object (&priv->location);
+        location_updated (source);
+}
+
+static void
+gclue_static_source_finalize (GObject *gstatic)
+{
+        GClueStaticSource *source = GCLUE_STATIC_SOURCE (gstatic);
+        GClueStaticSourcePrivate *priv = get_priv (source);
+
+        G_OBJECT_CLASS (gclue_static_source_parent_class)->finalize (gstatic);
+
+        close_file (source);
+
+        g_clear_object (&priv->location);
+        g_clear_handle_id (&priv->location_set_timer, g_source_remove);
+
+        g_clear_object (&priv->monitor);
+}
+
+static GClueLocationSourceStartResult
+gclue_static_source_start (GClueLocationSource *source)
+{
+        GClueLocationSourceClass *base_class;
+        GClueLocationSourceStartResult base_result;
+
+        g_return_val_if_fail (GCLUE_IS_STATIC_SOURCE (source),
+                              GCLUE_LOCATION_SOURCE_START_RESULT_FAILED);
+
+        base_class = GCLUE_LOCATION_SOURCE_CLASS (gclue_static_source_parent_class);
+        base_result = base_class->start (source);
+        if (base_result != GCLUE_LOCATION_SOURCE_START_RESULT_OK)
+                return base_result;
+
+        /* Set initial location */
+        location_set_refresh_timer (GCLUE_STATIC_SOURCE (source));
+
+        return base_result;
+}
+
+static void
+gclue_static_source_class_init (GClueStaticSourceClass *klass)
+{
+        GClueLocationSourceClass *source_class = GCLUE_LOCATION_SOURCE_CLASS (klass);
+        GObjectClass *gstatic_class = G_OBJECT_CLASS (klass);
+
+        gstatic_class->finalize = gclue_static_source_finalize;
+
+        source_class->start = gclue_static_source_start;
+}
+
+static void
+on_line_read (GObject *object,
+              GAsyncResult *result,
+              gpointer user_data)
+{
+        GDataInputStream *data_stream = G_DATA_INPUT_STREAM (object);
+        GClueStaticSource *source;
+        GClueStaticSourcePrivate *priv;
+        g_autoptr(GError) error = NULL;
+        g_autofree char *line = NULL;
+        char *comment_start;
+        gdouble accuracy;
+
+        line = g_data_input_stream_read_line_finish (data_stream, result,
+                                                     NULL, &error);
+        if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
+                return;
+        }
+
+        source = GCLUE_STATIC_SOURCE (user_data);
+        priv = get_priv (source);
+
+        if (line == NULL) {
+                if (error != NULL) {
+                        g_warning ("Static source error when reading file: %s",
+                                   error->message);
+                } else {
+                        g_warning ("Static source unexpected EOF reading file (truncated?)");
+                }
+
+                close_file_clear_location (source);
+                return;
+        }
+
+        comment_start = strchr (line, '#');
+        if (comment_start) {
+                *comment_start = '\0';
+        }
+
+        g_strstrip (line);
+
+        if (strlen (line) == 0) {
+                file_read_next_line (source);
+                return;
+        }
+
+        do {
+                gdouble * const coords[] = {
+                        /* L_LAT */ &priv->latitude,
+                        /* L_LON */ &priv->longitude,
+                        /* L_ALT */ &priv->altitude,
+                        /* L_ACCURACY */ &accuracy,
+                };
+                char *endptr;
+
+                g_assert (priv->file_line >= L_LAT &&
+                          priv->file_line <= L_ACCURACY);
+
+                *coords[priv->file_line] = g_ascii_strtod (line, &endptr);
+                if (errno != 0 || *endptr != '\0') {
+                        g_warning ("Static source invalid line %d '%s'",
+                                   priv->file_line, line);
+                        close_file_clear_location (source);
+                        return;
+                }
+        } while (FALSE);
+
+        if (priv->file_line < L_ACCURACY) {
+                priv->file_line++;
+                file_read_next_line (source);
+                return;
+        }
+
+        close_file (source);
+
+        g_debug ("Static source read a new location");
+        g_clear_object (&priv->location);
+        priv->location = gclue_location_new_full (priv->latitude,
+                                                  priv->longitude,
+                                                  accuracy,
+                                                  GCLUE_LOCATION_SPEED_UNKNOWN,
+                                                  GCLUE_LOCATION_HEADING_UNKNOWN,
+                                                  priv->altitude,
+                                                  0, "Static location");
+        g_assert (priv->location);
+        location_updated (source);
+}
+
+static void
+file_read_next_line (GClueStaticSource *source)
+{
+        GClueStaticSourcePrivate *priv = get_priv (source);
+
+        g_assert (priv->data_stream);
+        g_data_input_stream_read_line_async (priv->data_stream,
+                                             G_PRIORITY_DEFAULT,
+                                             priv->cancellable,
+                                             on_line_read, source);
+}
+
+static void
+on_file_open (GObject *source_object,
+              GAsyncResult *res,
+              gpointer user_data)
+{
+        GFile *file = G_FILE (source_object);
+        GClueStaticSource *source;
+        GClueStaticSourcePrivate *priv;
+        g_autoptr(GFileInputStream) file_stream = NULL;
+        g_autoptr(GError) error = NULL;
+
+        file_stream = g_file_read_finish (file, res, &error);
+        if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
+                return;
+        }
+
+        source = GCLUE_STATIC_SOURCE (user_data);
+        priv = get_priv (source);
+
+        if (error != NULL) {
+                if (!priv->file_open_quiet) {
+                        g_autofree char *parsename = NULL;
+
+                        parsename = g_file_get_parse_name (file);
+                        g_warning ("Static source failed to open '%s': %s",
+                                   parsename, error->message);
+                }
+
+                close_file_clear_location (source);
+                return;
+        }
+
+        g_return_if_fail (file_stream != NULL);
+        g_assert (!priv->file_stream);
+        priv->file_stream = g_steal_pointer (&file_stream);
+
+        g_assert (!priv->data_stream);
+        priv->data_stream = g_data_input_stream_new
+                (G_INPUT_STREAM (priv->file_stream));
+
+        priv->file_line = L_LAT;
+        file_read_next_line (source);
+}
+
+static void
+open_file (GClueStaticSource *source, GFile *file, gboolean quiet)
+{
+        GClueStaticSourcePrivate *priv = get_priv (source);
+
+        close_file (source);
+
+        priv->cancellable = g_cancellable_new ();
+        priv->file_open_quiet = quiet;
+        g_file_read_async (file, G_PRIORITY_DEFAULT, priv->cancellable,
+                           on_file_open, source);
+}
+
+static void
+on_monitor_event (GFileMonitor *monitor,
+                  GFile *file,
+                  GFile *other_file,
+                  GFileMonitorEvent event_type,
+                  gpointer user_data)
+{
+        GClueStaticSource *source = GCLUE_STATIC_SOURCE (user_data);
+        g_autofree char *basename = NULL;
+
+        if (event_type != G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT &&
+            event_type != G_FILE_MONITOR_EVENT_DELETED) {
+                return;
+        }
+
+        g_return_if_fail (file != NULL);
+        basename = g_file_get_basename (file);
+        if (basename == NULL ||
+            strcmp (basename, GEO_FILE_NAME) != 0) {
+                return;
+        }
+
+        if (event_type == G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT) {
+                g_debug ("Static source trying to re-load since " GEO_FILE_PATH " has changed");
+                open_file (source, file, FALSE);
+        } else { /* G_FILE_MONITOR_EVENT_DELETED */
+                g_debug ("Static source flushing location since " GEO_FILE_PATH " was deleted");
+                close_file_clear_location (source);
+        }
+}
+
+static void
+check_monitor (GClueStaticSource *source)
+{
+        GClueStaticSourcePrivate *priv = get_priv (source);
+        g_autoptr(GFile) geo_file = NULL;
+        g_autoptr(GError) error = NULL;
+
+        if (priv->monitor)
+                return;
+
+        geo_file = g_file_new_for_path (GEO_FILE_PATH);
+        priv->monitor = g_file_monitor_file (geo_file, G_FILE_MONITOR_NONE,
+                                             NULL, &error);
+        if (error != NULL) {
+                g_warning ("Static source failed to monitor '" GEO_FILE_PATH "': %s",
+                           error->message);
+                g_clear_object (&priv->monitor);
+                return;
+        }
+
+        g_assert (priv->monitor);
+        g_file_monitor_set_rate_limit (priv->monitor,
+                                       GEO_FILE_MONITOR_RATE_LIMIT);
+        g_signal_connect_object (G_OBJECT (priv->monitor), "changed",
+                                 G_CALLBACK (on_monitor_event),
+                                 source, 0);
+
+        g_debug ("Static source monitoring '" GEO_FILE_PATH "', trying initial load");
+        open_file (source, geo_file, TRUE);
+}
+
+static void
+gclue_static_source_init (GClueStaticSource *source)
+{
+        check_monitor (source);
+}
+
+/**
+ * gclue_static_source_get_singleton:
+ *
+ * Get the #GClueStaticSource singleton, for the specified max accuracy
+ * level @level.
+ *
+ * Returns: (transfer full): a new ref to #GClueStaticSource. Use g_object_unref()
+ * when done.
+ **/
+GClueStaticSource *
+gclue_static_source_get_singleton (GClueAccuracyLevel level)
+{
+        static GClueStaticSource *source[] = { NULL, NULL };
+        gboolean is_exact;
+        int i;
+
+        g_return_val_if_fail (level >= GCLUE_ACCURACY_LEVEL_CITY, NULL);
+        is_exact = level == GCLUE_ACCURACY_LEVEL_EXACT;
+
+        i = is_exact ? 0 : 1;
+        if (source[i] == NULL) {
+                source[i] = g_object_new (GCLUE_TYPE_STATIC_SOURCE,
+                                          "compute-movement", FALSE,
+                                          "scramble-location", !is_exact,
+                                          NULL);
+                g_object_add_weak_pointer (G_OBJECT (source[i]),
+                                           (gpointer) &source[i]);
+        } else {
+                g_object_ref (source[i]);
+                check_monitor (source[i]);
+        }
+
+        return source[i];
+}
diff --git a/src/gclue-static-source.h b/src/gclue-static-source.h
new file mode 100644
index 0000000..acf0f62
--- /dev/null
+++ b/src/gclue-static-source.h
@@ -0,0 +1,40 @@
+/* vim: set et ts=8 sw=8: */
+/*
+ * Copyright © 2022,2023 Oracle and/or its affiliates.
+ *
+ * Geoclue is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free
+ * Software Foundation; either version 2 of the License, or (at your option)
+ * any later version.
+ *
+ * Geoclue is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with Geoclue; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ */
+
+#ifndef GCLUE_STATIC_SOURCE_H
+#define GCLUE_STATIC_SOURCE_H
+
+#include <glib.h>
+#include "gclue-location-source.h"
+
+G_BEGIN_DECLS
+
+#define GCLUE_TYPE_STATIC_SOURCE gclue_static_source_get_type ()
+
+G_DECLARE_FINAL_TYPE (GClueStaticSource,
+                      gclue_static_source,
+                      GCLUE, STATIC_SOURCE,
+                      GClueLocationSource)
+
+GClueStaticSource *gclue_static_source_get_singleton (GClueAccuracyLevel level);
+
+G_END_DECLS
+
+#endif /* GCLUE_STATIC_SOURCE_H */
diff --git a/src/gclue-utils.h b/src/gclue-utils.h
new file mode 100644
index 0000000..cb8bfb2
--- /dev/null
+++ b/src/gclue-utils.h
@@ -0,0 +1,61 @@
+/* vim: set et ts=8 sw=8: */
+/*
+ * Geoclue is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free
+ * Software Foundation; either version 2 of the License, or (at your option)
+ * any later version.
+ *
+ * Geoclue is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with Geoclue; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ */
+
+#ifndef GCLUE_UTILS_H
+#define GCLUE_UTILS_H
+
+#include <glib.h>
+#include <string.h>
+
+G_BEGIN_DECLS
+
+#ifndef strnpbrk
+inline static const char *
+strnpbrk (const char *s, const char *accept, size_t n)
+{
+        const char *end;
+
+        for (end = s + n; s < end && *s != '\0'; s++) {
+                if (strchr (accept, *s)) {
+                        return s;
+                }
+        }
+
+        return NULL;
+}
+#endif
+
+#ifndef strnspn
+inline static size_t
+strnspn (const char *s, const char *accept, size_t n)
+{
+        const char *cur, *end;
+
+        for (cur = s, end = s + n; cur < end && *cur != '\0'; cur++) {
+                if (!strchr (accept, *cur)) {
+                        break;
+                }
+        }
+
+        return cur - s;
+}
+#endif
+
+G_END_DECLS
+
+#endif /* GCLUE_UTILS_H */
diff --git a/src/gclue-web-source.c b/src/gclue-web-source.c
index bfd70f7..92d94b8 100644
--- a/src/gclue-web-source.c
+++ b/src/gclue-web-source.c
@@ -27,6 +27,7 @@
 #include "gclue-web-source.h"
 #include "gclue-error.h"
 #include "gclue-location.h"
+#include "gclue-mozilla.h"
 
 /**
  * SECTION:gclue-web-source
@@ -36,32 +37,46 @@
  * Baseclass for all sources that solely use a web resource for geolocation.
  **/
 
-static gboolean
-get_internet_available (void);
 static void
 refresh_accuracy_level (GClueWebSource *web);
 
 struct _GClueWebSourcePrivate {
+        GCancellable *cancellable;
+
+        GClueAccuracyLevel accuracy_level;
+
         SoupSession *soup_session;
 
         SoupMessage *query;
+        const char *query_data_description;
 
         gulong network_changed_id;
         gulong connectivity_changed_id;
 
         guint64 last_submitted;
 
-        gboolean internet_available;
+        const char *locate_url;
+        const char *submit_url;
+        gboolean locate_url_reachable;
+        gboolean submit_url_reachable;
 };
 
+enum
+{
+        PROP_0,
+        PROP_ACCURACY_LEVEL,
+        LAST_PROP
+};
+static GParamSpec *gParamSpecs[LAST_PROP];
+
 G_DEFINE_ABSTRACT_TYPE_WITH_CODE (GClueWebSource,
                                   gclue_web_source,
                                   GCLUE_TYPE_LOCATION_SOURCE,
                                   G_ADD_PRIVATE (GClueWebSource))
 
-static void refresh_callback (SoupSession *session,
-                              SoupMessage *query,
-                              gpointer     user_data);
+static void refresh_callback (SoupSession  *session,
+                              GAsyncResult *result,
+                              gpointer      user_data);
 
 static void
 gclue_web_source_real_refresh_async (GClueWebSource      *source,
@@ -83,12 +98,11 @@ gclue_web_source_real_refresh_async (GClueWebSource      *source,
                 return;
         }
 
-        if (!get_internet_available ()) {
+        if (!source->priv->locate_url_reachable) {
                 g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_NETWORK_UNREACHABLE,
-                                         "Network unavailable");
+                                         "Cannot reach locate URL");
                 return;
         }
-        g_debug ("Network available");
 
         if (source->priv->query != NULL) {
                 g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_PENDING,
@@ -96,53 +110,59 @@ gclue_web_source_real_refresh_async (GClueWebSource      *source,
                 return;
         }
 
-        source->priv->query = GCLUE_WEB_SOURCE_GET_CLASS (source)->create_query (source, &local_error);
-
+        source->priv->query = GCLUE_WEB_SOURCE_GET_CLASS (source)->create_query
+                (source, &source->priv->query_data_description, &local_error);
         if (source->priv->query == NULL) {
                 g_task_return_error (task, g_steal_pointer (&local_error));
                 return;
         }
 
-        /* TODO handle cancellation */
-        soup_session_queue_message (source->priv->soup_session,
-                                    source->priv->query,
-                                    refresh_callback,
-                                    g_steal_pointer (&task));
+        soup_session_send_and_read_async (source->priv->soup_session,
+                                          source->priv->query,
+                                          G_PRIORITY_DEFAULT,
+                                          cancellable,
+                                          (GAsyncReadyCallback)refresh_callback,
+                                          g_steal_pointer (&task));
 }
 
 static void
-refresh_callback (SoupSession *session,
-                  SoupMessage *query,
-                  gpointer     user_data)
+refresh_callback (SoupSession  *session,
+                  GAsyncResult *result,
+                  gpointer      user_data)
 {
         g_autoptr(GTask) task = g_steal_pointer (&user_data);
         GClueWebSource *web;
+        g_autoptr(SoupMessage) query = NULL;
+        g_autoptr(GBytes) body = NULL;
         g_autoptr(GError) local_error = NULL;
         g_autofree char *contents = NULL;
         g_autofree char *str = NULL;
         g_autoptr(GClueLocation) location = NULL;
-        SoupURI *uri;
+        GUri *uri;
 
-        if (query->status_code == SOUP_STATUS_CANCELLED) {
-                g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_CANCELLED,
-                                         "Operation cancelled");
+        web = GCLUE_WEB_SOURCE (g_task_get_source_object (task));
+        query = g_steal_pointer (&web->priv->query);
+
+        body = soup_session_send_and_read_finish (session, result, &local_error);
+        if (!body) {
+                g_task_return_error (task, g_steal_pointer (&local_error));
                 return;
         }
 
-        web = GCLUE_WEB_SOURCE (g_task_get_source_object (task));
-        web->priv->query = NULL;
-
-        if (query->status_code != SOUP_STATUS_OK) {
+        if (soup_message_get_status (query) != SOUP_STATUS_OK) {
                 g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_FAILED,
-                                         "Failed to query location: %s", query->reason_phrase);
+                                         "Query location SOUP error: %s",
+                                         soup_message_get_reason_phrase (query));
                 return;
         }
 
-        contents = g_strndup (query->response_body->data, query->response_body->length);
+        contents = g_strndup (g_bytes_get_data (body, NULL), g_bytes_get_size (body));
         uri = soup_message_get_uri (query);
-        str = soup_uri_to_string (uri, FALSE);
+        str = g_uri_to_string (uri);
         g_debug ("Got following response from '%s':\n%s", str, contents);
-        location = GCLUE_WEB_SOURCE_GET_CLASS (web)->parse_response (web, contents, &local_error);
+        location = gclue_mozilla_parse_response (contents,
+                                                 web->priv->query_data_description,
+                                                 &local_error);
         if (local_error != NULL) {
                 g_task_return_error (task, g_steal_pointer (&local_error));
                 return;
@@ -177,24 +197,18 @@ query_callback (GObject      *source_object,
         location = GCLUE_WEB_SOURCE_GET_CLASS (web)->refresh_finish (web, result, &local_error);
 
         if (local_error != NULL &&
-            !g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_NOT_INITIALIZED)) {
-                g_warning ("Failed to query location: %s", local_error->message);
-                return;
+            !g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
+                if (!g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_NOT_INITIALIZED) &&
+                    !g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_PENDING)) {
+                        g_warning ("Failed to query location: %s",
+                                   local_error->message);
+                } else {
+                        g_debug ("Failed to query location: %s",
+                                 local_error->message);
+                }
         }
 }
 
-static gboolean
-get_internet_available (void)
-{
-        GNetworkMonitor *monitor = g_network_monitor_get_default ();
-        gboolean available;
-
-        available = (g_network_monitor_get_connectivity (monitor) ==
-                     G_NETWORK_CONNECTIVITY_FULL);
-
-        return available;
-}
-
 static void
 refresh_accuracy_level (GClueWebSource *web)
 {
@@ -203,7 +217,7 @@ refresh_accuracy_level (GClueWebSource *web)
         existing = gclue_location_source_get_available_accuracy_level
                         (GCLUE_LOCATION_SOURCE (web));
         new = GCLUE_WEB_SOURCE_GET_CLASS (web)->get_available_accuracy_level
-                        (web, web->priv->internet_available);
+                        (web, web->priv->locate_url_reachable);
         if (new != existing) {
                 g_debug ("Available accuracy level from %s: %u",
                          G_OBJECT_TYPE_NAME (web), new);
@@ -213,19 +227,141 @@ refresh_accuracy_level (GClueWebSource *web)
         }
 }
 
+static gboolean
+get_internet_available (void)
+{
+        GNetworkMonitor *monitor = g_network_monitor_get_default ();
+
+        return g_network_monitor_get_connectivity (monitor) ==
+                G_NETWORK_CONNECTIVITY_FULL;
+}
+
+static void
+locate_url_checked_cb (GObject      *source_object,
+                       GAsyncResult *result,
+                       gpointer      user_data)
+{
+        GNetworkMonitor *mon = G_NETWORK_MONITOR (source_object);
+        GClueWebSource *web;
+        gboolean reachable, last_reachable;
+        g_autoptr(GError) error = NULL;
+
+        reachable = g_network_monitor_can_reach_finish (mon, result, &error);
+        if (error && g_error_matches (error, G_IO_ERROR,
+                                      G_IO_ERROR_CANCELLED)) {
+                return; /* WebSource instance is finalized */
+        }
+
+        if (!reachable && get_internet_available ()) {
+                g_debug ("Locate URL not reachable, but Internet is available, overriding");
+                reachable = TRUE;
+        }
+
+        web = GCLUE_WEB_SOURCE (user_data);
+        last_reachable = web->priv->locate_url_reachable;
+        web->priv->locate_url_reachable = reachable;
+        if (last_reachable == reachable)
+                return; /* We already reacted to network change */
+
+        g_debug ("Network changed: %s",
+                 reachable ? "Enabling locate URL queries" :
+                             "Disabling locate URL queries");
+        if (reachable) {
+                GCLUE_WEB_SOURCE_GET_CLASS (web)->refresh_async
+                        (web, NULL, query_callback, NULL);
+        }
+}
+
+static void
+submit_url_checked_cb (GObject      *source_object,
+                       GAsyncResult *result,
+                       gpointer      user_data)
+{
+        GNetworkMonitor *mon = G_NETWORK_MONITOR (source_object);
+        GClueWebSource *web;
+        gboolean reachable, last_reachable;
+        g_autoptr(GError) error = NULL;
+
+        reachable = g_network_monitor_can_reach_finish (mon, result, &error);
+        if (error && g_error_matches (error, G_IO_ERROR,
+                                      G_IO_ERROR_CANCELLED)) {
+                return; /* WebSource instance is finalized */
+        }
+
+        if (!reachable && get_internet_available ()) {
+                g_debug ("Submit URL not reachable, but Internet is available, overriding");
+                reachable = TRUE;
+        }
+
+        web = GCLUE_WEB_SOURCE (user_data);
+        last_reachable = web->priv->submit_url_reachable;
+        web->priv->submit_url_reachable = reachable;
+        if (last_reachable == reachable) {
+                return;
+        }
+        g_debug ("Network changed: %s",
+                 reachable ? "Enabling submit URL queries" :
+                             "Disabling submit URL queries");
+}
+
 static void
-on_network_changed (GNetworkMonitor *monitor G_GNUC_UNUSED,
+cancellable_cancel_recreate (GClueWebSource *source)
+{
+        GClueWebSourcePrivate *priv = source->priv;
+
+        g_cancellable_cancel (priv->cancellable);
+
+        g_clear_object (&priv->cancellable);
+        priv->cancellable = g_cancellable_new ();
+}
+
+static void
+on_network_changed (GNetworkMonitor *unused_monitor G_GNUC_UNUSED,
                     gboolean         available G_GNUC_UNUSED,
                     gpointer         user_data)
 {
+        GNetworkMonitor *monitor = g_network_monitor_get_default ();
         GClueWebSource *web = GCLUE_WEB_SOURCE (user_data);
-        gboolean last_available = web->priv->internet_available;
-
-        web->priv->internet_available = get_internet_available ();
-        if (last_available == web->priv->internet_available)
-                return; /* We already reacted to network change */
+        g_autoptr(GSocketConnectable) submit_addr = NULL;
+        g_autoptr(GSocketConnectable) locate_addr = NULL;
+
+        cancellable_cancel_recreate (web);
+
+        if (web->priv->submit_url) {
+                submit_addr = g_network_address_parse_uri (web->priv->submit_url,
+                                                           80, NULL);
+                if (submit_addr) {
+                        g_network_monitor_can_reach_async (monitor,
+                                                           submit_addr,
+                                                           web->priv->cancellable,
+                                                           submit_url_checked_cb,
+                                                           web);
+                } else {
+                        g_warning ("Could not parse submit URL '%s'",
+                                   web->priv->submit_url);
+                        web->priv->submit_url_reachable = FALSE;
+                }
+        } else {
+                web->priv->submit_url_reachable = FALSE;
+        }
 
-        GCLUE_WEB_SOURCE_GET_CLASS (web)->refresh_async (web, NULL, query_callback, NULL);
+        if (web->priv->locate_url) {
+                locate_addr = g_network_address_parse_uri (web->priv->locate_url,
+                                                           80, NULL);
+                if (locate_addr) {
+                        g_network_monitor_can_reach_async (monitor,
+                                                           locate_addr,
+                                                           web->priv->cancellable,
+                                                           locate_url_checked_cb,
+                                                           web);
+                } else {
+                        g_warning ("Could not parse locate URL '%s'",
+                                   web->priv->locate_url);
+                        web->priv->locate_url_reachable = FALSE;
+                }
+        } else {
+                web->priv->locate_url_reachable = FALSE;
+        }
 }
 
 static void
@@ -241,6 +377,8 @@ gclue_web_source_finalize (GObject *gsource)
 {
         GClueWebSourcePrivate *priv = GCLUE_WEB_SOURCE (gsource)->priv;
 
+        g_cancellable_cancel (priv->cancellable);
+
         if (priv->network_changed_id) {
                 g_signal_handler_disconnect (g_network_monitor_get_default (),
                                              priv->network_changed_id);
@@ -253,15 +391,13 @@ gclue_web_source_finalize (GObject *gsource)
                 priv->connectivity_changed_id = 0;
         }
 
-        if (priv->query != NULL) {
-                g_debug ("Cancelling query");
-                soup_session_cancel_message (priv->soup_session,
-                                             priv->query,
-                                             SOUP_STATUS_CANCELLED);
-                priv->query = NULL;
+        if (priv->soup_session) {
+                soup_session_abort (priv->soup_session);
+                g_clear_object (&priv->soup_session);
         }
 
-        g_clear_object (&priv->soup_session);
+        g_clear_object (&priv->query);
+        g_clear_object (&priv->cancellable);
 
         G_OBJECT_CLASS (gclue_web_source_parent_class)->finalize (gsource);
 }
@@ -274,10 +410,8 @@ gclue_web_source_constructed (GObject *object)
 
         G_OBJECT_CLASS (gclue_web_source_parent_class)->constructed (object);
 
-        priv->soup_session = soup_session_new_with_options
-                        (SOUP_SESSION_REMOVE_FEATURE_BY_TYPE,
-                         SOUP_TYPE_PROXY_RESOLVER_DEFAULT,
-                         NULL);
+        priv->soup_session = soup_session_new ();
+        soup_session_set_proxy_resolver (priv->soup_session, NULL);
 
         monitor = g_network_monitor_get_default ();
         priv->network_changed_id =
@@ -295,6 +429,42 @@ gclue_web_source_constructed (GObject *object)
                             object);
 }
 
+static void
+gclue_web_source_get_property (GObject    *object,
+                               guint       prop_id,
+                               GValue     *value,
+                               GParamSpec *pspec)
+{
+        GClueWebSource *web = GCLUE_WEB_SOURCE (object);
+
+        switch (prop_id) {
+        case PROP_ACCURACY_LEVEL:
+                g_value_set_enum (value, web->priv->accuracy_level);
+                break;
+
+        default:
+                G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+        }
+}
+
+static void
+gclue_web_source_set_property (GObject      *object,
+                               guint         prop_id,
+                               const GValue *value,
+                               GParamSpec   *pspec)
+{
+        GClueWebSource *web = GCLUE_WEB_SOURCE (object);
+
+        switch (prop_id) {
+        case PROP_ACCURACY_LEVEL:
+                web->priv->accuracy_level = g_value_get_enum (value);
+                break;
+
+        default:
+                G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+        }
+}
+
 static void
 gclue_web_source_class_init (GClueWebSourceClass *klass)
 {
@@ -303,14 +473,28 @@ gclue_web_source_class_init (GClueWebSourceClass *klass)
         klass->refresh_async = gclue_web_source_real_refresh_async;
         klass->refresh_finish = gclue_web_source_real_refresh_finish;
 
+        gsource_class->get_property = gclue_web_source_get_property;
+        gsource_class->set_property = gclue_web_source_set_property;
         gsource_class->finalize = gclue_web_source_finalize;
         gsource_class->constructed = gclue_web_source_constructed;
+
+        gParamSpecs[PROP_ACCURACY_LEVEL] = g_param_spec_enum ("accuracy-level",
+                                                              "AccuracyLevel",
+                                                              "Max accuracy level",
+                                                              GCLUE_TYPE_ACCURACY_LEVEL,
+                                                              GCLUE_ACCURACY_LEVEL_CITY,
+                                                              G_PARAM_READWRITE |
+                                                              G_PARAM_CONSTRUCT_ONLY);
+        g_object_class_install_property (gsource_class,
+                                         PROP_ACCURACY_LEVEL,
+                                         gParamSpecs[PROP_ACCURACY_LEVEL]);
 }
 
 static void
 gclue_web_source_init (GClueWebSource *web)
 {
         web->priv = gclue_web_source_get_instance_private (web);
+        web->priv->cancellable = g_cancellable_new ();
 }
 
 /**
@@ -330,25 +514,34 @@ gclue_web_source_refresh (GClueWebSource *source)
 }
 
 static void
-submit_query_callback (SoupSession *session,
-                       SoupMessage *query,
-                       gpointer     user_data)
+submit_query_callback (SoupSession  *session,
+                       GAsyncResult *result,
+                       gpointer      user_data)
 {
-        SoupURI *uri;
-        g_autofree char *str = NULL;
+        g_autoptr(GBytes) body = NULL;
+        g_autoptr(GError) local_error = NULL;
+        SoupMessage *query;
+        g_autofree char *uri_str = NULL;
+        gint status_code;
 
-        uri = soup_message_get_uri (query);
-        str = soup_uri_to_string (uri, FALSE);
-        if (query->status_code != SOUP_STATUS_OK &&
-            query->status_code != SOUP_STATUS_NO_CONTENT) {
+        query = soup_session_get_async_result_message (session, result);
+        uri_str = g_uri_to_string (soup_message_get_uri (query));
+
+        body = soup_session_send_and_read_finish (session, result, &local_error);
+        if (!body) {
                 g_warning ("Failed to submit location data to '%s': %s",
-                           str,
-                           query->reason_phrase);
-		return;
-	}
+                           uri_str, local_error->message);
+                return;
+        }
+
+        status_code = soup_message_get_status (query);
+        if (status_code != SOUP_STATUS_OK && status_code != SOUP_STATUS_NO_CONTENT) {
+                g_warning ("Failed to submit location data to '%s': %s",
+                           uri_str, soup_message_get_reason_phrase (query));
+                return;
+        }
 
-        g_debug ("Successfully submitted location data to '%s'",
-                 str);
+        g_debug ("Successfully submitted location data to '%s'", uri_str);
 }
 
 #define SUBMISSION_ACCURACY_THRESHOLD 100
@@ -362,22 +555,24 @@ on_submit_source_location_notify (GObject    *source_object,
         GClueLocationSource *source = GCLUE_LOCATION_SOURCE (source_object);
         GClueWebSource *web = GCLUE_WEB_SOURCE (user_data);
         GClueLocation *location;
-        SoupMessage *query;
-        GError *error = NULL;
+        g_autoptr(SoupMessage) query = NULL;
+        g_autoptr(GError) error = NULL;
+
+        if (!web->priv->submit_url_reachable)
+                return;
 
         location = gclue_location_source_get_location (source);
         if (location == NULL ||
             gclue_location_get_accuracy (location) >
             SUBMISSION_ACCURACY_THRESHOLD ||
+            gclue_location_get_accuracy (location) ==
+            GCLUE_LOCATION_ACCURACY_UNKNOWN ||
             gclue_location_get_timestamp (location) <
             web->priv->last_submitted + SUBMISSION_TIME_THRESHOLD)
                 return;
 
         web->priv->last_submitted = gclue_location_get_timestamp (location);
 
-        if (!get_internet_available ())
-                return;
-
         query = GCLUE_WEB_SOURCE_GET_CLASS (web)->create_submit_query
                                         (web,
                                          location,
@@ -386,16 +581,17 @@ on_submit_source_location_notify (GObject    *source_object,
                 if (error != NULL) {
                         g_warning ("Failed to create submission query: %s",
                                    error->message);
-                        g_error_free (error);
                 }
 
                 return;
         }
 
-        soup_session_queue_message (web->priv->soup_session,
-                                    query,
-                                    submit_query_callback,
-                                    web);
+        soup_session_send_and_read_async (web->priv->soup_session,
+                                          query,
+                                          G_PRIORITY_DEFAULT,
+                                          NULL,
+                                          (GAsyncReadyCallback)submit_query_callback,
+                                          web);
 }
 
 /**
@@ -423,3 +619,17 @@ gclue_web_source_set_submit_source (GClueWebSource      *web,
 
         on_submit_source_location_notify (G_OBJECT (submit_source), NULL, web);
 }
+
+void
+gclue_web_source_set_locate_url (GClueWebSource *source,
+                                 const char     *url)
+{
+        source->priv->locate_url = url;
+}
+
+void
+gclue_web_source_set_submit_url (GClueWebSource *source,
+                                 const char     *url)
+{
+        source->priv->submit_url = url;
+}
diff --git a/src/gclue-web-source.h b/src/gclue-web-source.h
index ea8ea73..05068c6 100644
--- a/src/gclue-web-source.h
+++ b/src/gclue-web-source.h
@@ -71,13 +71,11 @@ struct _GClueWebSourceClass {
                                                   GError              **error);
 
         SoupMessage *     (*create_query)        (GClueWebSource *source,
+                                                  const char **query_data_description,
                                                   GError        **error);
         SoupMessage *     (*create_submit_query) (GClueWebSource  *source,
                                                   GClueLocation   *location,
                                                   GError         **error);
-        GClueLocation * (*parse_response)        (GClueWebSource *source,
-                                                  const char     *response,
-                                                  GError        **error);
         GClueAccuracyLevel (*get_available_accuracy_level)
                                                  (GClueWebSource *source,
                                                   gboolean        network_available);
@@ -86,6 +84,10 @@ struct _GClueWebSourceClass {
 void gclue_web_source_refresh           (GClueWebSource      *source);
 void gclue_web_source_set_submit_source (GClueWebSource      *source,
                                          GClueLocationSource *submit_source);
+void gclue_web_source_set_locate_url    (GClueWebSource      *source,
+                                         const char          *url);
+void gclue_web_source_set_submit_url    (GClueWebSource      *source,
+                                         const char          *url);
 
 G_END_DECLS
 
diff --git a/src/gclue-wifi.c b/src/gclue-wifi.c
index 2108195..cd20b0d 100644
--- a/src/gclue-wifi.c
+++ b/src/gclue-wifi.c
@@ -24,6 +24,7 @@
 #include <string.h>
 #include <config.h>
 #include "gclue-wifi.h"
+#include "gclue-3g.h"
 #include "gclue-config.h"
 #include "gclue-error.h"
 #include "gclue-mozilla.h"
@@ -49,6 +50,12 @@
  * full cache (excluding overheads). */
 #define CACHE_ENTRY_MAX_AGE_SECONDS (48 * 60 * 60)
 
+/* The signal strength can typically vary by ±5 for a stationary laptop, so
+ * match cache entries with that tolerance.
+ * In dBm units.
+ */
+#define CACHE_ENTRY_MATCH_SIGNAL_WINDOW 10
+
 /**
  * SECTION:gclue-wifi
  * @short_description: WiFi-based geolocation
@@ -78,7 +85,58 @@ gclue_wifi_refresh_finish (GClueWebSource  *source,
 static void
 disconnect_cache_prune_timeout (GClueWifi *wifi);
 
+typedef struct {
+        GArray *signals;
+        GClueLocation *location;
+} LocationCacheElement;
+
+static LocationCacheElement *
+location_cache_element_new (GArray *signals,
+                            GClueLocation *location)
+{
+        LocationCacheElement *element;
+
+        element = g_slice_new (LocationCacheElement);
+        element->signals = signals;
+        element->location = g_object_ref (location);
+        return element;
+}
+
+static void location_cache_element_free (gpointer data)
+{
+        LocationCacheElement *element = data;
+
+        if (element->signals)
+                g_array_free (element->signals, TRUE);
+        g_clear_object (&element->location);
+        g_slice_free (LocationCacheElement, element);
+}
+
+typedef struct {
+        GList *elements;
+} LocationCacheValue;
+
+static LocationCacheValue *
+location_cache_value_new (void)
+{
+        LocationCacheValue *value;
+
+        value = g_slice_new (LocationCacheValue);
+        value->elements = NULL;
+        return value;
+}
+
+static void location_cache_value_free (gpointer data)
+{
+        LocationCacheValue *value = data;
+
+        g_list_free_full (value->elements, location_cache_element_free);
+        g_slice_free (LocationCacheValue, value);
+}
+
 struct _GClueWifiPrivate {
+        GCancellable *intf_cancellable, *bss_cancellable;
+        GClueMozilla *mozilla;
         WPASupplicant *supplicant;
         WPAInterface *interface;
         GHashTable *bss_proxies;
@@ -92,9 +150,7 @@ struct _GClueWifiPrivate {
 
         guint scan_timeout;
 
-        GClueAccuracyLevel accuracy_level;
-
-        GHashTable *location_cache;  /* (element-type GVariant GClueLocation) (owned) */
+        GHashTable *location_cache;  /* (element-type GVariant LocationCacheValue) (owned) */
         guint cache_prune_timeout_id;
         guint cache_hits, cache_misses;
 
@@ -104,25 +160,14 @@ struct _GClueWifiPrivate {
 #endif
 };
 
-enum
-{
-        PROP_0,
-        PROP_ACCURACY_LEVEL,
-        LAST_PROP
-};
-static GParamSpec *gParamSpecs[LAST_PROP];
-
 static SoupMessage *
 gclue_wifi_create_query (GClueWebSource *source,
+                         const char **query_data_description,
                          GError        **error);
 static SoupMessage *
 gclue_wifi_create_submit_query (GClueWebSource  *source,
                                 GClueLocation   *location,
                                 GError         **error);
-static GClueLocation *
-gclue_wifi_parse_response (GClueWebSource *source,
-                           const char     *json,
-                           GError        **error);
 static GClueAccuracyLevel
 gclue_wifi_get_available_accuracy_level (GClueWebSource *source,
                                          gboolean        net_available);
@@ -150,6 +195,8 @@ gclue_wifi_finalize (GObject *gwifi)
 
         G_OBJECT_CLASS (gclue_wifi_parent_class)->finalize (gwifi);
 
+        g_cancellable_cancel (wifi->priv->intf_cancellable);
+
         disconnect_bss_signals (wifi);
         disconnect_cache_prune_timeout (wifi);
 
@@ -158,47 +205,13 @@ gclue_wifi_finalize (GObject *gwifi)
         g_clear_pointer (&wifi->priv->bss_proxies, g_hash_table_unref);
         g_clear_pointer (&wifi->priv->ignored_bss_proxies, g_hash_table_unref);
         g_clear_pointer (&wifi->priv->location_cache, g_hash_table_unref);
+        g_clear_object (&wifi->priv->mozilla);
+        g_clear_object (&wifi->priv->intf_cancellable);
 }
 
 static void
 gclue_wifi_constructed (GObject *object);
 
-static void
-gclue_wifi_get_property (GObject    *object,
-                         guint       prop_id,
-                         GValue     *value,
-                         GParamSpec *pspec)
-{
-        GClueWifi *wifi = GCLUE_WIFI (object);
-
-        switch (prop_id) {
-        case PROP_ACCURACY_LEVEL:
-                g_value_set_enum (value, wifi->priv->accuracy_level);
-                break;
-
-        default:
-                G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
-        }
-}
-
-static void
-gclue_wifi_set_property (GObject      *object,
-                         guint         prop_id,
-                         const GValue *value,
-                         GParamSpec   *pspec)
-{
-        GClueWifi *wifi = GCLUE_WIFI (object);
-
-        switch (prop_id) {
-        case PROP_ACCURACY_LEVEL:
-                wifi->priv->accuracy_level = g_value_get_enum (value);
-                break;
-
-        default:
-                G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
-        }
-}
-
 static void
 gclue_wifi_class_init (GClueWifiClass *klass)
 {
@@ -212,24 +225,10 @@ gclue_wifi_class_init (GClueWifiClass *klass)
         web_class->refresh_finish = gclue_wifi_refresh_finish;
         web_class->create_submit_query = gclue_wifi_create_submit_query;
         web_class->create_query = gclue_wifi_create_query;
-        web_class->parse_response = gclue_wifi_parse_response;
         web_class->get_available_accuracy_level =
                 gclue_wifi_get_available_accuracy_level;
-        gwifi_class->get_property = gclue_wifi_get_property;
-        gwifi_class->set_property = gclue_wifi_set_property;
         gwifi_class->finalize = gclue_wifi_finalize;
         gwifi_class->constructed = gclue_wifi_constructed;
-
-        gParamSpecs[PROP_ACCURACY_LEVEL] = g_param_spec_enum ("accuracy-level",
-                                                              "AccuracyLevel",
-                                                              "Max accuracy level",
-                                                              GCLUE_TYPE_ACCURACY_LEVEL,
-                                                              GCLUE_ACCURACY_LEVEL_CITY,
-                                                              G_PARAM_READWRITE |
-                                                              G_PARAM_CONSTRUCT_ONLY);
-        g_object_class_install_property (gwifi_class,
-                                         PROP_ACCURACY_LEVEL,
-                                         gParamSpecs[PROP_ACCURACY_LEVEL]);
 }
 
 static void
@@ -345,19 +344,24 @@ on_bss_proxy_ready (GObject      *source_object,
                     GAsyncResult *res,
                     gpointer      user_data)
 {
-        GClueWifi *wifi = GCLUE_WIFI (user_data);
+        GClueWifi *wifi;
         WPABSS *bss;
-        GError *error = NULL;
+        g_autoptr(GError) error = NULL;
         char ssid[MAX_SSID_LEN + 1] = { 0 };
 
         bss = wpa_bss_proxy_new_for_bus_finish (res, &error);
         if (bss == NULL) {
-                g_debug ("%s", error->message);
-                g_error_free (error);
+                if (error && !g_error_matches (error, G_IO_ERROR,
+                                               G_IO_ERROR_CANCELLED)) {
+                        g_warning ("BSS proxy setup failed: %s",
+                                   error->message);
+                }
 
                 return;
         }
 
+        wifi = GCLUE_WIFI (user_data);
+
         if (gclue_mozilla_should_ignore_bss (bss)) {
                 g_object_unref (bss);
 
@@ -365,7 +369,7 @@ on_bss_proxy_ready (GObject      *source_object,
         }
 
         get_ssid_from_bss (bss, ssid);
-        g_debug ("WiFi AP '%s' added.", ssid);
+        g_debug ("Got WiFi AP '%s'", ssid);
 
         if (wpa_bss_get_signal (bss) <= WIFI_SCAN_BSS_NOISE_LEVEL) {
                 const char *path;
@@ -396,13 +400,15 @@ on_bss_added (WPAInterface *object,
               GVariant     *properties,
               gpointer      user_data)
 {
+        GClueWifi *wifi = GCLUE_WIFI (user_data);
+
         wpa_bss_proxy_new_for_bus (G_BUS_TYPE_SYSTEM,
                                    G_DBUS_PROXY_FLAGS_NONE,
                                    "fi.w1.wpa_supplicant1",
                                    path,
-                                   NULL,
+                                   wifi->priv->bss_cancellable,
                                    on_bss_proxy_ready,
-                                   user_data);
+                                   wifi);
 }
 
 static gboolean
@@ -459,7 +465,7 @@ start_wifi_scan (GClueWifi *wifi)
 
         wpa_interface_call_scan (WPA_INTERFACE (priv->interface),
                                  args,
-                                 NULL,
+                                 priv->bss_cancellable,
                                  on_scan_call_done,
                                  wifi);
 }
@@ -492,7 +498,6 @@ on_scan_timeout (gpointer user_data)
         GClueWifi *wifi = GCLUE_WIFI (user_data);
         GClueWifiPrivate *priv = wifi->priv;
 
-        g_debug ("WiFi scan timeout.");
         priv->scan_timeout = 0;
 
         if (priv->interface == NULL)
@@ -503,6 +508,11 @@ on_scan_timeout (gpointer user_data)
         return G_SOURCE_REMOVE;
 }
 
+gboolean gclue_wifi_should_skip_bsss (GClueAccuracyLevel level)
+{
+        return level < GCLUE_ACCURACY_LEVEL_STREET;
+}
+
 static gboolean
 on_scan_wait_done (gpointer wifi)
 {
@@ -511,9 +521,13 @@ on_scan_wait_done (gpointer wifi)
         g_return_val_if_fail (GCLUE_IS_WIFI (wifi), G_SOURCE_REMOVE);
         priv = GCLUE_WIFI(wifi)->priv;
 
+        /* We have the latest scan result */
+        gclue_mozilla_set_wifi (priv->mozilla, wifi);
+
         if (priv->bss_list_changed) {
                 priv->bss_list_changed = FALSE;
-                g_debug ("Refreshing location…");
+                g_debug ("WiFi BSS list changed, refreshing location…");
+                gclue_mozilla_set_bss_dirty (priv->mozilla);
                 gclue_web_source_refresh (GCLUE_WEB_SOURCE (wifi));
         }
         priv->scan_wait_id = 0;
@@ -521,6 +535,15 @@ on_scan_wait_done (gpointer wifi)
         return G_SOURCE_REMOVE;
 }
 
+static GClueAccuracyLevel
+get_accuracy_level (GClueWifi *wifi)
+{
+        GClueAccuracyLevel level;
+
+        g_object_get (G_OBJECT (wifi), "accuracy-level", &level, NULL);
+        return level;
+}
+
 static void
 on_scan_done (WPAInterface *object,
               gboolean      success,
@@ -535,7 +558,6 @@ on_scan_done (WPAInterface *object,
 
                 return;
         }
-        g_debug ("WiFi scan completed");
 
         if (priv->interface == NULL)
                 return;
@@ -558,14 +580,14 @@ on_scan_done (WPAInterface *object,
          * user's location can change quickly. With low accuracy, we don't since
          * we wouldn't want to drain power unnecessarily.
          */
-        if (priv->accuracy_level >= GCLUE_ACCURACY_LEVEL_STREET)
+        if (get_accuracy_level (wifi) >= GCLUE_ACCURACY_LEVEL_STREET)
                 timeout = WIFI_SCAN_TIMEOUT_HIGH_ACCURACY;
         else
                 timeout = WIFI_SCAN_TIMEOUT_LOW_ACCURACY;
         priv->scan_timeout = g_timeout_add_seconds (timeout,
                                                     on_scan_timeout,
                                                     wifi);
-        g_debug ("Next scan scheduled in %u seconds", timeout);
+        g_debug ("WiFi scan done, next scheduled in %u seconds", timeout);
 }
 
 static void
@@ -573,20 +595,24 @@ on_scan_call_done (GObject      *source_object,
                    GAsyncResult *res,
                    gpointer      user_data)
 {
-        GClueWifi *wifi = GCLUE_WIFI (user_data);
-        GError *error = NULL;
+        g_autoptr(GError) error = NULL;
 
-        if (!wpa_interface_call_scan_finish
-                (WPA_INTERFACE (source_object),
-                 res,
-                 &error)) {
-                g_warning ("Scanning of WiFi networks failed: %s",
-                           error->message);
-                g_error_free (error);
+        if (!wpa_interface_call_scan_finish (WPA_INTERFACE (source_object),
+                                             res, &error)) {
+                GClueWifi *wifi;
 
-                cancel_wifi_scan (wifi);
+                if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
+                        return;
+                }
 
-                return;
+                wifi = GCLUE_WIFI (user_data);
+
+                if (error) {
+                        g_warning ("Scanning of WiFi networks failed: %s",
+                                   error->message);
+                }
+
+                cancel_wifi_scan (wifi);
         }
 }
 
@@ -605,6 +631,9 @@ connect_bss_signals (GClueWifi *wifi)
                 return;
         }
 
+        g_assert (!priv->bss_cancellable);
+        priv->bss_cancellable = g_cancellable_new ();
+
         start_wifi_scan (wifi);
 
         priv->bss_list_changed = TRUE;
@@ -633,6 +662,12 @@ disconnect_bss_signals (GClueWifi *wifi)
 {
         GClueWifiPrivate *priv = wifi->priv;
 
+        if (priv->bss_cancellable) {
+                g_debug ("Cancelling WiFi requests");
+                g_cancellable_cancel (priv->bss_cancellable);
+                g_clear_object (&priv->bss_cancellable);
+        }
+
         cancel_wifi_scan (wifi);
 
         if (priv->bss_added_id != 0) {
@@ -657,22 +692,46 @@ cache_prune (GClueWifi *wifi)
         GHashTableIter iter;
         gpointer value;
         guint64 cutoff_seconds;
-        guint old_cache_size;
+        guint old_cache_size, removed_elements = 0;
 
         old_cache_size = g_hash_table_size (priv->location_cache);
         cutoff_seconds = g_get_real_time () / G_USEC_PER_SEC - CACHE_ENTRY_MAX_AGE_SECONDS;
 
         g_hash_table_iter_init (&iter, priv->location_cache);
         while (g_hash_table_iter_next (&iter, NULL, &value)) {
-                GClueLocation *location = GCLUE_LOCATION (value);
-                guint64 timestamp_seconds = gclue_location_get_timestamp (location);
-
-                if (timestamp_seconds <= cutoff_seconds)
-                        g_hash_table_iter_remove (&iter);
+                LocationCacheValue *lcvalue = (LocationCacheValue *)value;
+                GList *l = lcvalue->elements;
+
+                g_assert (l);
+                while (l) {
+                        LocationCacheElement *element = (LocationCacheElement *)l->data;
+                        GList *lnext = l->next;
+
+                        /* Keep this location? */
+                        if (gclue_location_get_timestamp (element->location) >
+                            cutoff_seconds)
+                                goto next_el;
+
+                        location_cache_element_free (element);
+                        lcvalue->elements = g_list_delete_link (lcvalue->elements, l);
+                        removed_elements++;
+
+                        /* Deleted the last entry (element) in this hash bucket?
+                         * Remove this hash table entry then.
+                         */
+                        if (!lcvalue->elements) {
+                                g_assert (!lnext);
+                                g_hash_table_iter_remove (&iter);
+                        }
+
+                next_el:
+                        l = lnext;
+                }
         }
 
-        g_debug ("Pruned cache (old size: %u, new size: %u)",
-                 old_cache_size, g_hash_table_size (priv->location_cache));
+        g_debug ("Pruned cache (old size: %u, new size: %u, removed elements: %u)",
+                 old_cache_size, g_hash_table_size (priv->location_cache),
+                 removed_elements);
 }
 
 #if GLIB_CHECK_VERSION(2, 64, 0)
@@ -782,6 +841,8 @@ gclue_wifi_start (GClueLocationSource *source)
 static GClueLocationSourceStopResult
 gclue_wifi_stop (GClueLocationSource *source)
 {
+        GClueWifi *wifi = GCLUE_WIFI (source);
+        GClueWifiPrivate *priv = wifi->priv;
         GClueLocationSourceClass *base_class;
         GClueLocationSourceStopResult base_result;
 
@@ -795,6 +856,10 @@ gclue_wifi_stop (GClueLocationSource *source)
         disconnect_bss_signals (GCLUE_WIFI (source));
         disconnect_cache_prune_timeout (GCLUE_WIFI (source));
 
+        if (gclue_mozilla_test_set_wifi (priv->mozilla, wifi, NULL)) {
+                g_debug ("Removed us as the WiFi source on stop");
+        }
+
         return base_result;
 }
 
@@ -802,15 +867,15 @@ static GClueAccuracyLevel
 gclue_wifi_get_available_accuracy_level (GClueWebSource *source,
                                          gboolean        net_available)
 {
-        GClueWifiPrivate *priv = GCLUE_WIFI (source)->priv;
+        GClueWifi *wifi = GCLUE_WIFI (source);
+        GClueWifiPrivate *priv = wifi->priv;
 
         if (!net_available)
                 return GCLUE_ACCURACY_LEVEL_NONE;
-        else if (priv->interface != NULL &&
-                 priv->accuracy_level != GCLUE_ACCURACY_LEVEL_CITY)
-                return GCLUE_ACCURACY_LEVEL_STREET;
-        else
+        else if (!priv->interface)
                 return GCLUE_ACCURACY_LEVEL_CITY;
+        else
+                return MIN (get_accuracy_level (wifi), GCLUE_ACCURACY_LEVEL_STREET);
 }
 
 static void
@@ -818,18 +883,25 @@ on_interface_proxy_ready (GObject      *source_object,
                           GAsyncResult *res,
                           gpointer      user_data)
 {
-        GClueWifi *wifi = GCLUE_WIFI (user_data);
+        GClueWifi *wifi;
         WPAInterface *interface;
-        GError *error = NULL;
+        g_autoptr(GError) error = NULL;
 
         interface = wpa_interface_proxy_new_for_bus_finish (res, &error);
         if (interface == NULL) {
-                g_debug ("%s", error->message);
-                g_error_free (error);
+                if (error) {
+                        if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
+                                return;
+                        }
+
+                        g_warning ("Interface proxy add failed: %s",
+                                   error->message);
+                }
 
                 return;
         }
 
+        wifi = GCLUE_WIFI (user_data);
         if (wifi->priv->interface != NULL) {
                 g_object_unref (interface);
                 return;
@@ -860,7 +932,7 @@ on_interface_added (WPASupplicant *supplicant,
                                          G_DBUS_PROXY_FLAGS_NONE,
                                          "fi.w1.wpa_supplicant1",
                                          path,
-                                         NULL,
+                                         wifi->priv->intf_cancellable,
                                          on_interface_proxy_ready,
                                          wifi);
 }
@@ -888,14 +960,27 @@ on_interface_removed (WPASupplicant *supplicant,
         disconnect_bss_signals (wifi);
         g_clear_object (&wifi->priv->interface);
 
+        if (gclue_mozilla_test_set_wifi (priv->mozilla, wifi, NULL)) {
+                g_debug ("Removed interface was the WiFi source");
+        }
+
         gclue_web_source_refresh (GCLUE_WEB_SOURCE (wifi));
 }
 
 static void
 gclue_wifi_init (GClueWifi *wifi)
 {
+        GClueWebSource *web_source = GCLUE_WEB_SOURCE (wifi);
+
         wifi->priv = gclue_wifi_get_instance_private (wifi);
 
+        wifi->priv->intf_cancellable = g_cancellable_new ();
+        wifi->priv->mozilla = gclue_mozilla_get_singleton ();
+        gclue_web_source_set_locate_url (web_source,
+                                         gclue_mozilla_get_locate_url (wifi->priv->mozilla));
+        gclue_web_source_set_submit_url (web_source,
+                                         gclue_mozilla_get_submit_url (wifi->priv->mozilla));
+
         wifi->priv->bss_proxies = g_hash_table_new_full (g_str_hash,
                                                          g_str_equal,
                                                          g_free,
@@ -907,7 +992,7 @@ gclue_wifi_init (GClueWifi *wifi)
         wifi->priv->location_cache = g_hash_table_new_full (variant_hash,
                                                             g_variant_equal,
                                                             (GDestroyNotify) g_variant_unref,
-                                                            g_object_unref);
+                                                            location_cache_value_free);
 }
 
 static void
@@ -916,11 +1001,11 @@ gclue_wifi_constructed (GObject *object)
         GClueWifi *wifi = GCLUE_WIFI (object);
         GClueWifiPrivate *priv = wifi->priv;
         const gchar *const *interfaces;
-        GError *error = NULL;
+        g_autoptr(GError) error = NULL;
 
         G_OBJECT_CLASS (gclue_wifi_parent_class)->constructed (object);
 
-        if (wifi->priv->accuracy_level == GCLUE_ACCURACY_LEVEL_CITY) {
+        if (get_accuracy_level (wifi) == GCLUE_ACCURACY_LEVEL_CITY) {
                 GClueConfig *config = gclue_config_get_singleton ();
 
                 if (!gclue_config_get_enable_wifi_source (config))
@@ -936,20 +1021,20 @@ gclue_wifi_constructed (GObject *object)
                          NULL,
                          &error);
         if (priv->supplicant == NULL) {
-                g_warning ("Failed to connect to wpa_supplicant service: %s",
-                           error->message);
-                g_error_free (error);
+                if (error)
+                        g_warning ("Failed to connect to wpa_supplicant service: %s",
+                                   error->message);
                 goto refresh_n_exit;
         }
 
-        g_signal_connect (priv->supplicant,
-                          "interface-added",
-                          G_CALLBACK (on_interface_added),
-                          wifi);
-        g_signal_connect (priv->supplicant,
-                          "interface-removed",
-                          G_CALLBACK (on_interface_removed),
-                          wifi);
+        g_signal_connect_object (priv->supplicant,
+                                 "interface-added",
+                                 G_CALLBACK (on_interface_added),
+                                 wifi, 0);
+        g_signal_connect_object (priv->supplicant,
+                                 "interface-removed",
+                                 G_CALLBACK (on_interface_removed),
+                                 wifi, 0);
 
         interfaces = wpa_supplicant_get_interfaces (priv->supplicant);
         if (interfaces != NULL && interfaces[0] != NULL)
@@ -982,23 +1067,29 @@ on_wifi_destroyed (gpointer data,
 GClueWifi *
 gclue_wifi_get_singleton (GClueAccuracyLevel level)
 {
-        static GClueWifi *wifi[] = { NULL, NULL };
+        static GClueWifi *wifi[] = { NULL, NULL, NULL };
         guint i;
+        GClueConfig *config = gclue_config_get_singleton ();
+        gboolean wifi_enabled;
         gboolean scramble_location = FALSE;
         gboolean compute_movement = FALSE;
 
         g_return_val_if_fail (level >= GCLUE_ACCURACY_LEVEL_CITY, NULL);
-        if (level == GCLUE_ACCURACY_LEVEL_NEIGHBORHOOD)
-                level = GCLUE_ACCURACY_LEVEL_CITY;
 
+        wifi_enabled = gclue_config_get_enable_wifi_source (config);
         if (level == GCLUE_ACCURACY_LEVEL_CITY) {
-                GClueConfig *config = gclue_config_get_singleton ();
-
                 i = 0;
-                if (gclue_config_get_enable_wifi_source (config))
+                if (wifi_enabled)
                         scramble_location = TRUE;
-        } else {
+        } else if (level == GCLUE_ACCURACY_LEVEL_NEIGHBORHOOD) {
+                g_return_val_if_fail (wifi_enabled, NULL);
+
                 i = 1;
+                scramble_location = TRUE;
+        } else {
+                g_return_val_if_fail (wifi_enabled, NULL);
+
+                i = 2;
                 compute_movement = TRUE;
         }
 
@@ -1017,38 +1108,33 @@ gclue_wifi_get_singleton (GClueAccuracyLevel level)
         return wifi[i];
 }
 
-GClueAccuracyLevel
-gclue_wifi_get_accuracy_level (GClueWifi *wifi)
+static gboolean
+wifi_should_skip_tower (GClueWifi *wifi)
 {
-        g_return_val_if_fail (GCLUE_IS_WIFI (wifi),
-                              GCLUE_ACCURACY_LEVEL_NONE);
-
-        return wifi->priv->accuracy_level;
+        return gclue_3g_should_skip_tower (get_accuracy_level (wifi));
 }
 
 /* Can return NULL, signifying an empty BSS list. */
-static GList *
-get_bss_list (GClueWifi *wifi)
+GList *
+gclue_wifi_get_bss_list (GClueWifi *wifi)
 {
         return g_hash_table_get_values (wifi->priv->bss_proxies);
 }
 
 static SoupMessage *
 gclue_wifi_create_query (GClueWebSource *source,
+                         const char **query_data_description,
                          GError        **error)
 {
         GClueWifi *wifi = GCLUE_WIFI (source);
-        GList *bss_list = NULL; /* As in Access Points */
-        SoupMessage *msg;
+        gboolean skip_tower;
 
         if (wifi->priv->interface == NULL) {
                 goto create_query;
         }
 
-        bss_list = get_bss_list (wifi);
-
         /* Empty list? */
-        if (bss_list == NULL) {
+        if (!g_hash_table_size (wifi->priv->bss_proxies)) {
                 g_set_error_literal (error,
                                      G_IO_ERROR,
                                      G_IO_ERROR_FAILED,
@@ -1057,17 +1143,13 @@ gclue_wifi_create_query (GClueWebSource *source,
         }
 
 create_query:
-        msg = gclue_mozilla_create_query (bss_list, NULL, error);
-        g_list_free (bss_list);
-        return msg;
-}
+        skip_tower = wifi_should_skip_tower (wifi);
+        if (skip_tower) {
+                g_debug ("Will skip 3GPP tower in query due to our accuracy level");
+        }
 
-static GClueLocation *
-gclue_wifi_parse_response (GClueWebSource *source,
-                           const char     *json,
-                           GError        **error)
-{
-        return gclue_mozilla_parse_response (json, error);
+        return gclue_mozilla_create_query (wifi->priv->mozilla, skip_tower, FALSE,
+                                           query_data_description, error);
 }
 
 static SoupMessage *
@@ -1076,7 +1158,6 @@ gclue_wifi_create_submit_query (GClueWebSource  *source,
                                 GError         **error)
 {
         GClueWifi *wifi = GCLUE_WIFI (source);
-        GList *bss_list; /* As in Access Points */
         SoupMessage * msg;
 
         if (wifi->priv->interface == NULL) {
@@ -1087,10 +1168,8 @@ gclue_wifi_create_submit_query (GClueWebSource  *source,
                 return NULL;
         }
 
-        bss_list = get_bss_list (wifi);
-
         /* Empty list? */
-        if (bss_list == NULL) {
+        if (!g_hash_table_size (wifi->priv->bss_proxies)) {
                 g_set_error_literal (error,
                                      G_IO_ERROR,
                                      G_IO_ERROR_FAILED,
@@ -1098,11 +1177,9 @@ gclue_wifi_create_submit_query (GClueWebSource  *source,
                 return NULL;
         }
 
-        msg = gclue_mozilla_create_submit_query (location,
-                                                 bss_list,
-                                                 NULL,
+        msg = gclue_mozilla_create_submit_query (wifi->priv->mozilla,
+                                                 location,
                                                  error);
-        g_list_free (bss_list);
         return msg;
 }
 
@@ -1145,20 +1222,46 @@ variant_hash (gconstpointer key)
         return g_bytes_hash (bytes);
 }
 
-static GVariant *
-get_location_cache_key (GClueWifi *wifi)
+static void location_cache_key_fill_tower (GClueWifi *wifi, GClue3GTower *tower)
+{
+        GClueWifiPrivate *priv = wifi->priv;
+        GClue3GTower *moztower;
+
+        memset (tower, 0, sizeof (*tower));
+        tower->tec = GCLUE_TOWER_TEC_NO_FIX;
+
+        moztower = gclue_mozilla_get_tower (priv->mozilla);
+        if (!moztower || wifi_should_skip_tower (wifi)) {
+                return;
+        }
+
+        g_assert (moztower->tec != GCLUE_TOWER_TEC_NO_FIX);
+        *tower = *moztower;
+}
+
+static void location_cache_key_add_tower (GClueWifi *wifi, GVariantBuilder *builder)
+{
+        GClue3GTower tower;
+
+        location_cache_key_fill_tower (wifi, &tower);
+        g_variant_builder_add (builder, "u", (guint32)tower.tec);
+        g_variant_builder_add (builder, "s", tower.opc);
+        g_variant_builder_add (builder, "t", (guint64)tower.lac);
+        g_variant_builder_add (builder, "t", (guint64)tower.cell_id);
+}
+
+static GPtrArray *
+get_location_cache_bss_array (GClueWifi *wifi)
 {
         GHashTableIter iter;
         gpointer value;
         g_autoptr(GPtrArray) bss_array = g_ptr_array_new_with_free_func (NULL);  /* (element-type WPABSS) */
-        guint i;
-        GVariantBuilder builder;
 
         /* The Mozilla service puts BSSID and signal strength for each BSS into
-         * its query. The signal strength can typically vary by ±5 for a
-         * stationary laptop, so quantise by that. Pack the whole lot into a
-         * #GVariant for simplicity, sorted by MAC address. The sorting has to
-         * happen in an array beforehand, as variants are immutable. */
+         * its query. Pack the whole lot into a #GVariant for simplicity, sorted
+         * by MAC address. The sorting has to happen in an array beforehand,
+         * as variants are immutable.
+         */
         g_hash_table_iter_init (&iter, wifi->priv->bss_proxies);
 
         while (g_hash_table_iter_next (&iter, NULL, &value)) {
@@ -1169,39 +1272,142 @@ get_location_cache_key (GClueWifi *wifi)
 
         g_ptr_array_sort (bss_array, bss_compare);
 
+        return g_steal_pointer (&bss_array);
+}
+
+static GVariant *
+get_location_cache_hashtable_key (GClueWifi *wifi, GPtrArray *bss_array)
+{
+        guint i;
+        GVariantBuilder builder;
+
         /* Serialise to a variant. */
-        g_variant_builder_init (&builder, G_VARIANT_TYPE ("a(ayn)"));
+        g_variant_builder_init (&builder, G_VARIANT_TYPE ("(usttaay)"));
+        location_cache_key_add_tower (wifi, &builder);
+
+        g_variant_builder_open (&builder, G_VARIANT_TYPE ("aay"));
         for (i = 0; i < bss_array->len; i++) {
                 WPABSS *bss = WPA_BSS (bss_array->pdata[i]);
                 GVariant *bssid;
 
-                g_variant_builder_open (&builder, G_VARIANT_TYPE ("(ayn)"));
-
                 bssid = wpa_bss_get_bssid (bss);
                 if (bssid == NULL)
                         continue;
 
                 g_variant_builder_add_value (&builder, bssid);
-                g_variant_builder_add (&builder, "n", wpa_bss_get_signal (bss) / 10);
-
-                g_variant_builder_close (&builder);
         }
+        g_variant_builder_close (&builder);
 
         return g_variant_builder_end (&builder);
 }
 
+static GArray *
+get_location_cache_signal_array (GClueWifi *wifi, GPtrArray *bss_array)
+{
+        g_autoptr(GArray) signal_array = NULL;
+        guint i;
+
+        signal_array = g_array_sized_new (FALSE, FALSE, sizeof (gint16), bss_array->len);
+        for (i = 0; i < bss_array->len; i++) {
+                WPABSS *bss = WPA_BSS (bss_array->pdata[i]);
+                gint16 signal = wpa_bss_get_signal (bss);
+
+                g_array_append_val (signal_array, signal);
+        }
+
+        return g_steal_pointer (&signal_array);
+}
+
+static gboolean cached_signals_match (GArray *signals1, GArray *signals2)
+{
+        guint i;
+
+        if (signals1->len != signals2->len) {
+                g_warning ("Different signal count in one hash table entry: %u vs %u",
+                           signals1->len, signals2->len);
+                return FALSE;
+        }
+
+        for (i = 0; i < signals1->len; i++) {
+                gint s1 = g_array_index (signals1, gint16, i);
+                gint s2 = g_array_index (signals2, gint16, i);
+
+                if (ABS (s1 - s2) > CACHE_ENTRY_MATCH_SIGNAL_WINDOW / 2)
+                        return FALSE;
+        }
+
+        return TRUE;
+}
+
 static GClueLocation *
-duplicate_location_new_timestamp (GClueLocation *location)
+find_cached_location (GHashTable *cache, GVariant *key, GArray *signals)
+{
+        g_autofree gchar *key_str = g_variant_print (key, FALSE);
+        GClueLocation *location = NULL;
+        LocationCacheValue *value;
+        GList *l;
+
+        value = g_hash_table_lookup (cache, key);
+        if (!value) {
+                g_debug ("Cache miss for key %s", key_str);
+                return NULL;
+        }
+
+        g_assert (value->elements);
+        for (l = value->elements; l; l = l->next) {
+                LocationCacheElement *element = l->data;
+
+                if (location &&
+                    gclue_location_get_accuracy (element->location) >=
+                    gclue_location_get_accuracy (location)) {
+                        /* Have at least as accurate location already,
+                         * don't bother with comparing signals.
+                         */
+                        continue;
+                }
+
+                if (!cached_signals_match (element->signals, signals))
+                        continue;
+
+                location = element->location;
+        }
+
+        if (location) {
+                g_debug ("Cache hit for key %s: got location %p (%s)",
+                         key_str, location,
+                         gclue_location_get_description (location));
+        } else {
+                g_debug ("Cache had key %s, but with different signals", key_str);
+        }
+
+        return location;
+}
+
+typedef struct {
+        GVariant *cache_key;
+        GArray *signals;
+} RefreshTaskData;
+
+static RefreshTaskData *
+refresh_task_data_new (GVariant *cache_key,
+                       GArray *signals)
 {
-        return g_object_new (GCLUE_TYPE_LOCATION,
-                             "latitude", gclue_location_get_latitude (location),
-                             "longitude", gclue_location_get_longitude (location),
-                             "accuracy", gclue_location_get_accuracy (location),
-                             "altitude", gclue_location_get_altitude (location),
-                             "timestamp", 0,
-                             "speed", gclue_location_get_speed (location),
-                             "heading", gclue_location_get_heading (location),
-                             NULL);
+        RefreshTaskData *tdata;
+
+        tdata = g_slice_new (RefreshTaskData);
+        tdata->cache_key = g_variant_ref (cache_key);
+        tdata->signals = signals;
+        return tdata;
+}
+
+static void refresh_task_data_free (gpointer data)
+{
+        RefreshTaskData *rdata = data;
+
+        g_clear_pointer (&rdata->cache_key, g_variant_unref);
+        if (rdata->signals)
+                g_array_free (rdata->signals, TRUE);
+        g_slice_free (RefreshTaskData, rdata);
 }
 
 static void
@@ -1212,39 +1418,58 @@ gclue_wifi_refresh_async (GClueWebSource      *source,
 {
         GClueWifi *wifi = GCLUE_WIFI (source);
         g_autoptr(GTask) task = g_task_new (source, cancellable, callback, user_data);
-        g_autoptr(GVariant) cache_key = get_location_cache_key (wifi);
-        g_autofree gchar *cache_key_str = g_variant_print (cache_key, FALSE);
-        GClueLocation *cached_location = g_hash_table_lookup (wifi->priv->location_cache, cache_key);
+        g_autoptr(GPtrArray) bss_array = get_location_cache_bss_array (wifi);
+        g_autoptr(GVariant) cache_key = get_location_cache_hashtable_key (wifi, bss_array);
+        g_autoptr(GArray) signal_array = get_location_cache_signal_array (wifi, bss_array);
+        GClueLocation *cached_location = find_cached_location (wifi->priv->location_cache,
+                                                               cache_key, signal_array);
+        RefreshTaskData *tdata;
 
         g_task_set_source_tag (task, gclue_wifi_refresh_async);
-        g_task_set_task_data (task, g_steal_pointer (&cache_key), (GDestroyNotify) g_variant_unref);
 
         if (gclue_location_source_get_active (GCLUE_LOCATION_SOURCE (source))) {
                 /* Try the cache. */
                 if (cached_location != NULL) {
                         g_autoptr(GClueLocation) new_location = NULL;
 
-                        g_debug ("Cache hit for key %s: got location %p (%s)",
-                                 cache_key_str, cached_location,
-                                 gclue_location_get_description (cached_location));
                         wifi->priv->cache_hits++;
 
                         /* Duplicate the location so its timestamp is updated. */
-                        new_location = duplicate_location_new_timestamp (cached_location);
+                        new_location = gclue_location_duplicate_fresh (cached_location);
                         gclue_location_source_set_location (GCLUE_LOCATION_SOURCE (source), new_location);
 
                         g_task_return_pointer (task, g_steal_pointer (&new_location), g_object_unref);
                         return;
                 }
 
-                g_debug ("Cache miss for key %s; querying web service", cache_key_str);
                 wifi->priv->cache_misses++;
         }
 
+        tdata = refresh_task_data_new (cache_key, g_steal_pointer (&signal_array));
+        g_task_set_task_data (task, tdata, refresh_task_data_free);
+
         /* Fall back to querying the web service. */
         GCLUE_WEB_SOURCE_CLASS (gclue_wifi_parent_class)->refresh_async (source, cancellable, refresh_cb, g_steal_pointer (&task));
 }
 
+static void
+add_cached_location (GHashTable *cache,
+                     GVariant *key, GArray **signals,
+                     GClueLocation *location)
+{
+        LocationCacheValue *value;
+        LocationCacheElement *element;
+
+        value = g_hash_table_lookup (cache, key);
+        if (!value) {
+                value = location_cache_value_new ();
+                g_hash_table_insert (cache, g_variant_ref (key), value);
+        }
+
+        element = location_cache_element_new (g_steal_pointer (signals), location);
+        value->elements = g_list_prepend (value->elements, element);
+}
+
 static void
 refresh_cb (GObject      *source_object,
             GAsyncResult *result,
@@ -1255,7 +1480,7 @@ refresh_cb (GObject      *source_object,
         g_autoptr(GTask) task = g_steal_pointer (&user_data);
         g_autoptr(GClueLocation) location = NULL;
         g_autoptr(GError) local_error = NULL;
-        GVariant *cache_key;
+        RefreshTaskData *tdata;
         g_autofree gchar *cache_key_str = NULL;
         double cache_hit_ratio;
 
@@ -1268,9 +1493,11 @@ refresh_cb (GObject      *source_object,
         }
 
         /* Cache the result. */
-        cache_key = g_task_get_task_data (task);
-        cache_key_str = g_variant_print (cache_key, FALSE);
-        g_hash_table_replace (wifi->priv->location_cache, g_variant_ref (cache_key), g_object_ref (location));
+        tdata = g_task_get_task_data (task);
+        cache_key_str = g_variant_print (tdata->cache_key, FALSE);
+        add_cached_location (wifi->priv->location_cache,
+                             tdata->cache_key, &tdata->signals,
+                             location);
 
         if (wifi->priv->cache_hits || wifi->priv->cache_misses) {
                 double cache_attempts;
diff --git a/src/gclue-wifi.h b/src/gclue-wifi.h
index abb4912..29ee9ce 100644
--- a/src/gclue-wifi.h
+++ b/src/gclue-wifi.h
@@ -63,7 +63,8 @@ struct _GClueWifiClass {
 };
 
 GClueWifi *        gclue_wifi_get_singleton      (GClueAccuracyLevel level);
-GClueAccuracyLevel gclue_wifi_get_accuracy_level (GClueWifi *wifi);
+gboolean gclue_wifi_should_skip_bsss (GClueAccuracyLevel level);
+GList *gclue_wifi_get_bss_list (GClueWifi *wifi);
 
 G_END_DECLS
 
diff --git a/src/meson.build b/src/meson.build
index 13eb1ba..b5175f8 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -1,5 +1,5 @@
 geoclue_deps = base_deps + [ dependency('json-glib-1.0', version: '>= 0.14.0'),
-                             dependency('libsoup-2.4', version: '>= 2.42.0') ]
+                             dependency('libsoup-3.0', version: '>= 3.0.0') ]
 
 sources = [ libgeoclue_public_api_gen_sources[1],
             geoclue_iface_sources,
@@ -24,14 +24,16 @@ sources += [ 'gclue-main.c',
              'gclue-service-manager.h', 'gclue-service-manager.c',
              'gclue-service-client.h', 'gclue-service-client.c',
              'gclue-service-location.h', 'gclue-service-location.c',
+             'gclue-static-source.c', 'gclue-static-source.h',
              'gclue-web-source.c', 'gclue-web-source.h',
              'gclue-wifi.h', 'gclue-wifi.c',
              'gclue-mozilla.h', 'gclue-mozilla.c',
              'gclue-min-uint.h', 'gclue-min-uint.c',
-             'gclue-location.h', 'gclue-location.c' ]
+             'gclue-location.h', 'gclue-location.c',
+             'gclue-utils.h' ]
 
 if get_option('3g-source') or get_option('cdma-source') or get_option('modem-gps-source')
-    geoclue_deps += [ dependency('mm-glib', version: '>= 1.10') ]
+    geoclue_deps += [ dependency('mm-glib', version: '>= 1.12') ]
     sources += [ 'gclue-modem.c',
 				 'gclue-modem.h',
 				 'gclue-modem-manager.c',
-- 
GitLab