summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMaciej S. Szmigiero <maciej.szmigiero@oracle.com>2023-01-05 20:47:24 +0100
committerMaciej S. Szmigiero <maciej.szmigiero@oracle.com>2023-01-15 13:50:23 +0100
commitcb66669cff959046c8ef02abe13fc4f1a68660c5 (patch)
treed969ebeecb694d25b007068242916f435d77c7f0
parent0cc334a44ed0384785379996e7f20e85f26e624f (diff)
downloadgeoclue-cb66669cff959046c8ef02abe13fc4f1a68660c5.tar.gz
Add static location source
There were many requests to provide a static location source for systems which don't normally move but where the existing location sources provide poor location estimate. An example of such system would be a desktop PC without WiFi or 3G modem. So let's add a source that reads location from "geolocation" file in @sysconfdir@ (normally /etc) to cover this use case. This file is constantly monitored for changes during Geoclue operation and the reported static location is updated accordingly. The geoclue(5) man page should be consulted for the format description of this file.
-rw-r--r--README.md5
-rw-r--r--data/geoclue.5.in52
-rw-r--r--data/geoclue.conf.in13
-rw-r--r--data/meson.build1
-rw-r--r--src/gclue-config.c17
-rw-r--r--src/gclue-config.h2
-rw-r--r--src/gclue-location-source.h2
-rw-r--r--src/gclue-locator.c32
-rw-r--r--src/gclue-static-source.c491
-rw-r--r--src/gclue-static-source.h40
-rw-r--r--src/meson.build1
11 files changed, 647 insertions, 9 deletions
diff --git a/README.md b/README.md
index 6242120..b483157 100644
--- a/README.md
+++ b/README.md
@@ -6,13 +6,14 @@ 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)
+- Static location source (reads location from a system-wide file)
WiFi-based geolocation makes use of
[Mozilla Location Service](https://wiki.mozilla.org/CloudServices/Location).
diff --git a/data/geoclue.5.in b/data/geoclue.5.in
index 137dde1..cc7a099 100644
--- a/data/geoclue.5.in
+++ b/data/geoclue.5.in
@@ -98,6 +98,20 @@ Compass configuration options
.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
.B allowed=true
@@ -179,6 +193,44 @@ 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 AUTHOR
.na
.nf
diff --git a/data/geoclue.conf.in b/data/geoclue.conf.in
index 8006cda..9afb5e0 100644
--- a/data/geoclue.conf.in
+++ b/data/geoclue.conf.in
@@ -43,6 +43,8 @@ enable=true
[wifi]
# Enable WiFi source
+# If this source and the static source below are both disabled a GeoIP-only
+# source will be used instead.
enable=true
# URL to the WiFi geolocation service. If not set, defaults to Mozilla's
@@ -86,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/meson.build b/data/meson.build
index a1fc61f..fc8dac5 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;')
diff --git a/src/gclue-config.c b/src/gclue-config.c
index bd52b7c..2802484 100644
--- a/src/gclue-config.c
+++ b/src/gclue-config.c
@@ -44,6 +44,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;
@@ -123,7 +124,7 @@ 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;
g_auto(GStrv) groups = NULL;
@@ -317,6 +318,13 @@ load_compass_config (GClueConfig *config)
}
static void
+load_static_source_config (GClueConfig *config)
+{
+ config->priv->enable_static_source =
+ load_enable_source_config (config, "static-source");
+}
+
+static void
gclue_config_init (GClueConfig *config)
{
g_autoptr(GError) error = NULL;
@@ -342,6 +350,7 @@ gclue_config_init (GClueConfig *config)
load_modem_gps_config (config);
load_network_nmea_config (config);
load_compass_config (config);
+ load_static_source_config (config);
}
GClueConfig *
@@ -530,3 +539,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.h b/src/gclue-location-source.h
index 620f601..a1969ba 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);
diff --git a/src/gclue-locator.c b/src/gclue-locator.c
index d9e8cdc..fd6266c 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"
@@ -357,7 +360,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;
@@ -377,12 +380,20 @@ 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 ();
@@ -405,6 +416,15 @@ gclue_locator_constructed (GObject *object)
}
#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",
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/meson.build b/src/meson.build
index ca6c91f..7a6d020 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -24,6 +24,7 @@ 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',