diff --git a/meson.build b/meson.build index f81e5974..0198a6d1 100644 --- a/meson.build +++ b/meson.build @@ -10,7 +10,7 @@ project( 'c_std=gnu89', 'warning_level=2', ], - meson_version: '>= 0.53.0', + meson_version: '>= 0.60.0', ) meson_version = meson.version() @@ -31,11 +31,13 @@ mm_includedir = get_option('includedir') mm_libdir = get_option('libdir') mm_sbindir = get_option('sbindir') mm_sysconfdir = get_option('sysconfdir') +mm_sharedstatedir = get_option('sharedstatedir') mm_pkgdatadir = mm_datadir / mm_name mm_pkgincludedir = mm_includedir / mm_name mm_pkglibdir = mm_libdir / mm_name mm_pkgsysconfdir = mm_sysconfdir / mm_name +mm_pkgsharedstatedir = mm_sharedstatedir / mm_name mm_glib_name = 'libmm-glib' mm_glib_pkgincludedir = mm_includedir / mm_glib_name @@ -168,21 +170,21 @@ glib_deps = declare_dependency( # DBus system directory dbus_dep = dependency('dbus-1') -dbus_interfaces_dir = dbus_dep.get_pkgconfig_variable('interfaces_dir', define_variable: ['datadir', mm_datadir]) -dbus_system_bus_services_dir = dbus_dep.get_pkgconfig_variable('system_bus_services_dir', define_variable: ['datadir', mm_datadir]) +dbus_interfaces_dir = dbus_dep.get_variable(pkgconfig: 'interfaces_dir', pkgconfig_define: ['datadir', mm_datadir]) +dbus_system_bus_services_dir = dbus_dep.get_variable(pkgconfig: 'system_bus_services_dir', pkgconfig_define: ['datadir', mm_datadir]) dbus_policy_dir = get_option('dbus_policy_dir') if dbus_policy_dir == '' - dbus_policy_dir = dbus_dep.get_pkgconfig_variable('sysconfdir', define_variable: ['sysconfdir', mm_sysconfdir]) / 'dbus-1/system.d' + dbus_policy_dir = dbus_dep.get_variable(pkgconfig: 'sysconfdir', pkgconfig_define: ['sysconfdir', mm_sysconfdir]) / 'dbus-1/system.d' endif enable_bash_completion = get_option('bash_completion') if enable_bash_completion bash_completion_dep = dependency('bash-completion') - bash_completion_completionsdir = bash_completion_dep.get_pkgconfig_variable( - 'completionsdir', + bash_completion_completionsdir = bash_completion_dep.get_variable( + pkgconfig: 'completionsdir', # bash-completion 2.10 changed the substitutions - define_variable: bash_completion_dep.version().version_compare('>= 2.10') ? ['datadir', mm_datadir] : ['prefix', mm_prefix], + pkgconfig_define: bash_completion_dep.version().version_compare('>= 2.10') ? ['datadir', mm_datadir] : ['prefix', mm_prefix], ) endif @@ -197,7 +199,7 @@ config_h.set('WITH_UDEV', enable_udev) udev_udevdir = get_option('udevdir') if udev_udevdir == '' assert(enable_udev, 'udevdir must be explicitly given if udev support is disabled') - udev_udevdir = dependency('udev').get_pkgconfig_variable('udevdir') + udev_udevdir = dependency('udev').get_variable(pkgconfig: 'udevdir') endif udev_rulesdir = udev_udevdir / 'rules.d' @@ -207,7 +209,7 @@ install_systemdunitdir = (systemd_systemdsystemunitdir != 'no') if install_systemdunitdir and systemd_systemdsystemunitdir == '' systemd_dep = dependency('systemd', not_found_message: 'systemd required but not found, please provide a valid systemd user unit dir or disable it') - systemd_systemdsystemunitdir = systemd_dep.get_pkgconfig_variable('systemdsystemunitdir', define_variable: ['root_prefix', mm_prefix]) + systemd_systemdsystemunitdir = systemd_dep.get_variable(pkgconfig: 'systemdsystemunitdir', pkgconfig_define: ['root_prefix', mm_prefix]) endif # Suspend/resume support @@ -240,7 +242,7 @@ enable_polkit = (polkit_opt != 'no') if enable_polkit polkit_gobject_dep = dependency('polkit-gobject-1', version: '>= 0.97', not_found_message: 'PolicyKit development headers are required') - polkit_gobject_policydir = polkit_gobject_dep.get_pkgconfig_variable('policydir', define_variable: ['prefix', mm_prefix]) + polkit_gobject_policydir = polkit_gobject_dep.get_variable(pkgconfig: 'policydir', pkgconfig_define: ['prefix', mm_prefix]) policy_conf = {'MM_DEFAULT_USER_POLICY': (polkit_opt == 'permissive' ? 'yes' : 'auth_self_keep')} endif @@ -297,7 +299,7 @@ if enable_qrtr if qmi_glib_dep.type_name() == 'internal' qmi_qrtr_supported = qmi_glib_proj.get_variable('enable_qrtr') else - qmi_qrtr_supported = qmi_glib_dep.get_pkgconfig_variable('qmi_qrtr_supported') + qmi_qrtr_supported = qmi_glib_dep.get_variable(pkgconfig: 'qmi_qrtr_supported') endif assert(qmi_qrtr_supported.to_int().is_odd(), 'Couldn\'t find QRTR support in qmi-glib.') qrtr_glib_dep = dependency('qrtr-glib', version: '>= 1.0.0') diff --git a/src/meson.build b/src/meson.build index 04d199d1..78d8794d 100644 --- a/src/meson.build +++ b/src/meson.build @@ -46,6 +46,7 @@ sources = files( 'mm-cbm-part.c', 'mm-charsets.c', 'mm-error-helpers.c', + 'mm-location-cache.c', 'mm-log.c', 'mm-log-object.c', 'mm-modem-helpers.c', @@ -80,11 +81,16 @@ if enable_systemd_journal private_deps += libsystemd_dep endif +c_args = [ + '-DPKGSTATEDIR="@0@"'.format(mm_pkgsharedstatedir), +] + libhelpers = static_library( 'helpers', sources: sources + enums_sources, include_directories: incs, dependencies: deps + private_deps, + c_args: c_args, ) libhelpers_dep = declare_dependency( @@ -506,6 +512,11 @@ install_data( install_dir: udev_rulesdir, ) +# empty directory for cached position +install_emptydir( + mm_pkgsharedstatedir, +) + if enable_tests subdir('tests') endif diff --git a/src/mm-location-cache.c b/src/mm-location-cache.c new file mode 100644 index 00000000..e27921ff --- /dev/null +++ b/src/mm-location-cache.c @@ -0,0 +1,241 @@ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This program 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. + * + * This program 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: + * + * Copyright (C) 2026 Andrey Skvortsov + */ + + +#include +#include + +#define _LIBMM_INSIDE_MM +#include + +#include "mm-location-cache.h" +#include "mm-log-object.h" + +#if !defined PKGSTATEDIR +# error PKGSTATEDIR is not defined +#endif + +#define LOCATION_STATE_FILE "location.ini" +#define LOCATION_LAST_POS_GROUP "last_position" +#define LOCATION_LAST_POS_LATITUDE_KEY "latitude" +#define LOCATION_LAST_POS_LONGITUDE_KEY "longitude" + +/*****************************************************************************/ + +static void log_object_iface_init (MMLogObjectInterface *iface); + +G_DEFINE_TYPE_EXTENDED (MMLocationCache, mm_location_cache, G_TYPE_OBJECT, 0, + G_IMPLEMENT_INTERFACE (MM_TYPE_LOG_OBJECT, log_object_iface_init)) + +struct _MMLocationCachePrivate { + MMLocationGpsRaw *gps_raw; + gdouble last_latitude; + gdouble last_longitude; + gchar* filename; +}; + +/*****************************************************************************/ + +static gchar * +log_object_build_id (MMLogObject *_self) +{ + return g_strdup ("location-cache"); +} + +gboolean +mm_location_cache_load_from_file (MMLocationCache *self, + const gchar *file, + GError **error) +{ + g_autoptr(GKeyFile) key_file = g_key_file_new (); + gdouble lat; + gdouble lon; + + self->priv->last_latitude = MM_LOCATION_LATITUDE_UNKNOWN; + self->priv->last_longitude = MM_LOCATION_LONGITUDE_UNKNOWN; + + if (!g_key_file_load_from_file (key_file, file, G_KEY_FILE_NONE, error)) { + g_prefix_error (error, "Error loading cached location from %s: ", file); + return FALSE; + } + + lat = g_key_file_get_double (key_file, + LOCATION_LAST_POS_GROUP, + LOCATION_LAST_POS_LATITUDE_KEY, + error); + if (*error) { + g_prefix_error (error, "Error finding latitude in file: "); + return FALSE; + } + + lon = g_key_file_get_double (key_file, + LOCATION_LAST_POS_GROUP, + LOCATION_LAST_POS_LONGITUDE_KEY, + error); + if (*error) { + g_prefix_error (error, "Error finding longitude in file: "); + return FALSE; + } + + self->priv->last_latitude = lat; + self->priv->last_longitude = lon; + return TRUE; +} + +gboolean +mm_location_cache_load (MMLocationCache *self, + GError **error) +{ + return mm_location_cache_load_from_file (self, self->priv->filename, error); +} + +gboolean +mm_location_cache_save_to_file (MMLocationCache *self, + const gchar *file, + GError **error) +{ + g_autoptr(GKeyFile) key_file = g_key_file_new (); + + if ((self->priv->last_latitude == MM_LOCATION_LATITUDE_UNKNOWN) || + (self->priv->last_longitude == MM_LOCATION_LONGITUDE_UNKNOWN)) { + return TRUE; + } + + key_file = g_key_file_new (); + g_key_file_set_double (key_file, + LOCATION_LAST_POS_GROUP, + LOCATION_LAST_POS_LATITUDE_KEY, + self->priv->last_latitude); + g_key_file_set_double (key_file, + LOCATION_LAST_POS_GROUP, + LOCATION_LAST_POS_LONGITUDE_KEY, + self->priv->last_longitude); + + if (!g_key_file_save_to_file (key_file, file, error)) { + g_prefix_error (error, "Error saving cached location to %s", file); + return FALSE; + } + + return TRUE; +} + +gboolean +mm_location_cache_save (MMLocationCache *self, + GError **error) +{ + return mm_location_cache_save_to_file (self, self->priv->filename, error); +} + +void +mm_location_cache_set_filename (MMLocationCache *self, + const gchar *file) +{ + g_free (self->priv->filename); + self->priv->filename = g_strdup (file); +} + +void +mm_location_cache_update_from_lat_lon (MMLocationCache *self, + const double lat, + const double lon) +{ + if (lat != MM_LOCATION_LATITUDE_UNKNOWN && + lon != MM_LOCATION_LONGITUDE_UNKNOWN) { + self->priv->last_latitude = lat; + self->priv->last_longitude = lon; + } +} + +void +mm_location_cache_update_from_nmea (MMLocationCache *self, + const gchar *trace) +{ + gdouble lat; + gdouble lon; + + g_assert (self->priv->gps_raw != NULL); + if (mm_location_gps_raw_add_trace (self->priv->gps_raw, trace)) { + lat = mm_location_gps_raw_get_latitude (self->priv->gps_raw); + lon = mm_location_gps_raw_get_longitude (self->priv->gps_raw); + mm_location_cache_update_from_lat_lon (self, lat, lon); + } +} + +void +mm_location_cache_get_lat_lon (MMLocationCache *self, + double *lat, + double *lon) +{ + *lat = self->priv->last_latitude; + *lon = self->priv->last_longitude; +} + +MMLocationCache * +mm_location_cache_new (void) +{ + return MM_LOCATION_CACHE (g_object_new (MM_TYPE_LOCATION_CACHE, NULL)); +} + +static void +mm_location_cache_init (MMLocationCache *self) +{ + self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self, MM_TYPE_LOCATION_CACHE, MMLocationCachePrivate); + + self->priv->last_longitude = MM_LOCATION_LONGITUDE_UNKNOWN; + self->priv->last_latitude = MM_LOCATION_LATITUDE_UNKNOWN; + self->priv->gps_raw = mm_location_gps_raw_new (); + + self->priv->filename = g_build_path (G_DIR_SEPARATOR_S, PKGSTATEDIR, LOCATION_STATE_FILE, NULL); +} + +static void +finalize (GObject *object) +{ + MMLocationCache *self = MM_LOCATION_CACHE (object); + + g_object_unref (self->priv->gps_raw); + g_free (self->priv->filename); + + G_OBJECT_CLASS (mm_location_cache_parent_class)->finalize (object); +} + +static void +dispose (GObject *object) +{ + g_autoptr(GError) error = NULL; + MMLocationCache *self = MM_LOCATION_CACHE (object); + + if (!mm_location_cache_save (self, &error)) + mm_obj_warn (self, "%s", error->message); + + G_OBJECT_CLASS (mm_location_cache_parent_class)->dispose (object); +} + +static void +log_object_iface_init (MMLogObjectInterface *iface) +{ + iface->build_id = log_object_build_id; +} + +static void +mm_location_cache_class_init (MMLocationCacheClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + g_type_class_add_private (object_class, sizeof (MMLocationCachePrivate)); + + object_class->finalize = finalize; + object_class->dispose = dispose; +} diff --git a/src/mm-location-cache.h b/src/mm-location-cache.h new file mode 100644 index 00000000..6c8ed291 --- /dev/null +++ b/src/mm-location-cache.h @@ -0,0 +1,57 @@ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This program 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. + * + * This program 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: + * + * Copyright (C) 2026 Andrey Skvortsov + */ + +#ifndef MM_LOCATION_CACHE_H +#define MM_LOCATION_CACHE_H + +#include +#include + +#define MM_TYPE_LOCATION_CACHE (mm_location_cache_get_type ()) +#define MM_LOCATION_CACHE(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), MM_TYPE_LOCATION_CACHE, MMLocationCache)) +#define MM_LOCATION_CACHE_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), MM_TYPE_LOCATION_CACHE, MMLocationCacheClass)) +#define MM_IS_LOCATION_CACHE(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), MM_TYPE_LOCATION_CACHE)) +#define MM_IS_LOCATION_CACHE_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), MM_TYPE_LOCATION_CACHE)) +#define MM_LOCATION_CACHE_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), MM_TYPE_LOCATION_CACHE, MMLocationCacheClass)) + +typedef struct _MMLocationCache MMLocationCache; +typedef struct _MMLocationCacheClass MMLocationCacheClass; +typedef struct _MMLocationCachePrivate MMLocationCachePrivate; + +struct _MMLocationCache { + GObject parent; + MMLocationCachePrivate *priv; +}; + +struct _MMLocationCacheClass { + GObjectClass parent; +}; + +GType mm_location_cache_get_type (void); +G_DEFINE_AUTOPTR_CLEANUP_FUNC (MMLocationCache, g_object_unref) + +MMLocationCache *mm_location_cache_new (void); + +gboolean mm_location_cache_load_from_file (MMLocationCache *self, const gchar *file, GError **error); +gboolean mm_location_cache_load (MMLocationCache *self, GError **error); +gboolean mm_location_cache_save_to_file (MMLocationCache *self, const gchar *file, GError **error); +gboolean mm_location_cache_save (MMLocationCache *self, GError **error); +void mm_location_cache_set_filename (MMLocationCache *self, const gchar *file); +void mm_location_cache_update_from_lat_lon (MMLocationCache *self, const double lat, const double lon); +void mm_location_cache_update_from_nmea (MMLocationCache *self, const gchar *nmea); +void mm_location_cache_get_lat_lon (MMLocationCache *self, double *lat, double *lon); + +#endif /* MM_LOCATION_CACHE_H */ + diff --git a/src/mm-shared-qmi.c b/src/mm-shared-qmi.c index 055829b5..d93698a6 100644 --- a/src/mm-shared-qmi.c +++ b/src/mm-shared-qmi.c @@ -30,6 +30,7 @@ #include "mm-iface-modem.h" #include "mm-iface-modem-3gpp.h" #include "mm-iface-modem-location.h" +#include "mm-location-cache.h" #include "mm-sim-qmi.h" #include "mm-shared-qmi.h" #include "mm-modem-helpers-qmi.h" @@ -91,6 +92,9 @@ typedef struct { guint32 loc_assistance_data_max_file_size; guint32 loc_assistance_data_max_part_size; + gulong loc_assistance_inject_time_req_indication_id; + gulong loc_assistance_inject_position_req_indication_id; + /* Carrier config helpers */ gboolean config_active_default; GArray *config_list; @@ -102,6 +106,9 @@ typedef struct { gulong uim_slot_status_indication_id; gulong uim_refresh_indication_id; guint uim_refresh_start_timeout_id; + + /* Cached last location from nmea */ + MMLocationCache *location_cache; } Private; static void @@ -119,6 +126,10 @@ private_free (Private *priv) g_object_unref (priv->pds_client); if (priv->loc_location_nmea_indication_id) g_signal_handler_disconnect (priv->loc_client, priv->loc_location_nmea_indication_id); + if (priv->loc_assistance_inject_time_req_indication_id) + g_signal_handler_disconnect (priv->loc_client, priv->loc_assistance_inject_time_req_indication_id); + if (priv->loc_assistance_inject_position_req_indication_id) + g_signal_handler_disconnect (priv->loc_client, priv->loc_assistance_inject_position_req_indication_id); if (priv->loc_client) g_object_unref (priv->loc_client); if (priv->uim_slot_status_indication_id) @@ -131,6 +142,7 @@ private_free (Private *priv) g_object_unref (priv->uim_client); if (priv->uim_refresh_start_timeout_id) g_source_remove (priv->uim_refresh_start_timeout_id); + g_clear_object (&priv->location_cache); if (priv->feature_nas_ssp_acquisition_order_preference_array) g_array_unref (priv->feature_nas_ssp_acquisition_order_preference_array); g_strfreev (priv->loc_assistance_data_servers); @@ -140,7 +152,8 @@ private_free (Private *priv) static Private * get_private (MMSharedQmi *self) { - Private *priv; + g_autoptr(GError) error = NULL; + Private *priv; if (G_UNLIKELY (!private_quark)) private_quark = g_quark_from_static_string (PRIVATE_TAG); @@ -155,6 +168,10 @@ get_private (MMSharedQmi *self) priv->feature_nas_ssp_acquisition_order_preference = FEATURE_UNKNOWN; priv->config_active_i = -1; + priv->location_cache = mm_location_cache_new (); + if (!mm_location_cache_load (priv->location_cache, &error)) + mm_obj_warn (priv->location_cache, "%s", error->message); + /* Setup parent class' MMIfaceModemLocation */ g_assert (MM_SHARED_QMI_GET_INTERFACE (self)->peek_parent_location_interface); priv->iface_modem_location_parent = MM_SHARED_QMI_GET_INTERFACE (self)->peek_parent_location_interface (self); @@ -5156,13 +5173,17 @@ stop_gps_engine (MMSharedQmi *self, GAsyncReadyCallback callback, gpointer user_data) { - GTask *task; - Private *priv; + GTask *task; + Private *priv; + g_autoptr(GError) error = NULL; priv = get_private (self); task = g_task_new (self, NULL, callback, user_data); + if (!mm_location_cache_save (priv->location_cache, &error)) + mm_obj_warn (self, "%s", error->message); + if (priv->pds_client) { QmiMessagePdsSetGpsServiceStateInput *input; @@ -5209,7 +5230,9 @@ pds_location_event_report_indication_cb (QmiClientPds *clie { QmiPdsPositionSessionStatus session_status; const gchar *nmea; + Private *priv; + priv = get_private (MM_SHARED_QMI (self)); if (qmi_indication_pds_event_report_output_get_position_session_status ( output, &session_status, @@ -5224,6 +5247,7 @@ pds_location_event_report_indication_cb (QmiClientPds *clie NULL)) { mm_obj_dbg (self, "[NMEA] %s", nmea); mm_iface_modem_location_gps_update (MM_IFACE_MODEM_LOCATION (self), nmea); + mm_location_cache_update_from_nmea (priv->location_cache, nmea); } } @@ -5233,13 +5257,16 @@ loc_location_nmea_indication_cb (QmiClientLoc *client, MMSharedQmi *self) { const gchar *nmea = NULL; + Private *priv; + priv = get_private (MM_SHARED_QMI (self)); qmi_indication_loc_nmea_output_get_nmea_string (output, &nmea, NULL); if (!nmea) return; mm_obj_dbg (self, "[NMEA] %s", nmea); mm_iface_modem_location_gps_update (MM_IFACE_MODEM_LOCATION (self), nmea); + mm_location_cache_update_from_nmea (priv->location_cache, nmea); } /*****************************************************************************/ @@ -5670,6 +5697,7 @@ loc_start_ready (QmiClientLoc *client, { QmiMessageLocRegisterEventsInput *input; QmiMessageLocStartOutput *output; + guint64 event_mask; GError *error = NULL; output = qmi_client_loc_start_finish (client, res, &error); @@ -5691,8 +5719,11 @@ loc_start_ready (QmiClientLoc *client, qmi_message_loc_start_output_unref (output); input = qmi_message_loc_register_events_input_new (); + event_mask = QMI_LOC_EVENT_REGISTRATION_FLAG_NMEA | + QMI_LOC_EVENT_REGISTRATION_FLAG_INJECT_TIME_REQUEST | + QMI_LOC_EVENT_REGISTRATION_FLAG_INJECT_POSITION_REQUEST; qmi_message_loc_register_events_input_set_event_registration_mask ( - input, QMI_LOC_EVENT_REGISTRATION_FLAG_NMEA, NULL); + input, event_mask, NULL); qmi_client_loc_register_events (client, input, 10, @@ -6714,6 +6745,338 @@ loc_location_get_predicted_orbits_data_source_indication_timed_out (GTask *task) return G_SOURCE_REMOVE; } +/*****************************************************************************/ +/* Location: Inject UTC time */ + +typedef struct { + guint timeout_id; + gulong indication_id; + QmiClientLoc *client; +} InjectTimeContext; + +static void +inject_time_context_cleanup_action (InjectTimeContext *ctx) +{ + if (ctx->timeout_id) { + g_source_remove (ctx->timeout_id); + ctx->timeout_id = 0; + } + if (ctx->indication_id) { + if (ctx->client) + g_signal_handler_disconnect (ctx->client, ctx->indication_id); + ctx->indication_id = 0; + } +} + +static void +inject_time_context_free (InjectTimeContext *ctx) +{ + inject_time_context_cleanup_action (ctx); + g_clear_object (&ctx->client); + g_slice_free (InjectTimeContext, ctx); +} + +static void +inject_time_finish_task (GTask *_task, + GError *error) +{ + InjectTimeContext *ctx; + g_autoptr(GTask) task = _task; + + ctx = g_task_get_task_data (task); + inject_time_context_cleanup_action (ctx); + if (error) + g_task_return_error (task, error); + else + g_task_return_boolean (task, TRUE); +} + +static gboolean +loc_location_inject_time_indication_timed_out (GTask *task) +{ + inject_time_finish_task (task, g_error_new (MM_CORE_ERROR, MM_CORE_ERROR_ABORTED, + "Operation timed out")); + return G_SOURCE_REMOVE; +} + +static void +loc_location_inject_time_indication_cb (QmiClientLoc *client, + QmiIndicationLocInjectUtcTimeOutput *output, + GTask *task) +{ + QmiLocIndicationStatus status; + GError *error = NULL; + + if (!qmi_indication_loc_inject_utc_time_output_get_indication_status (output, &status, &error)) + g_prefix_error (&error, "QMI operation failed: "); + else + mm_error_from_qmi_loc_indication_status (status, &error); + inject_time_finish_task (task, error); +} + +static void +loc_location_inject_time_ready (QmiClientLoc *client, + GAsyncResult *res, + GTask *task) +{ + g_autoptr(QmiMessageLocInjectUtcTimeOutput) output = NULL; + GError *error = NULL; + + output = qmi_client_loc_inject_utc_time_finish (client, res, &error); + if (!output) { + g_prefix_error (&error, "QMI operation failed: "); + inject_time_finish_task (task, error); + return; + } + + if (!qmi_message_loc_inject_utc_time_output_get_result (output, &error)) { + inject_time_finish_task (task, error); + return; + } +} + +static void +loc_location_inject_time_req_indication_cb (QmiClientLoc *client, + QmiIndicationLocInjectTimeRequestOutput *output, + MMSharedQmi *self) +{ + g_autoptr(QmiMessageLocInjectUtcTimeInput) input = NULL; + guint64 utc_ms = 0; + GTask *task; + InjectTimeContext *ctx; + + task = g_task_new (self, NULL, NULL, NULL); + ctx = g_slice_new0 (InjectTimeContext); + g_task_set_task_data (task, ctx, (GDestroyNotify)inject_time_context_free); + + ctx->client = g_object_ref (client); + + input = qmi_message_loc_inject_utc_time_input_new (); + + utc_ms = g_get_real_time () / 1000; + qmi_message_loc_inject_utc_time_input_set_utc_time (input, utc_ms, NULL); + qmi_message_loc_inject_utc_time_input_set_time_uncertainty (input, 1000, NULL); + qmi_message_loc_inject_utc_time_input_set_time_source (input, QMI_LOC_INJECTED_TIME_SOURCE_UNKNOWN, NULL); + + ctx->timeout_id = g_timeout_add_seconds (10, + (GSourceFunc) loc_location_inject_time_indication_timed_out, + task); + + ctx->indication_id = g_signal_connect (client, + "inject-utc-time", + G_CALLBACK (loc_location_inject_time_indication_cb), + task); + + qmi_client_loc_inject_utc_time ( + QMI_CLIENT_LOC (ctx->client), + input, + 10, + NULL, /* cancellable */ + (GAsyncReadyCallback)loc_location_inject_time_ready, + task); +} + +/*****************************************************************************/ +/* Location: Inject position */ + +typedef struct { + guint timeout_id; + gulong indication_id; + QmiClientLoc *client; +} InjectPositionContext; + +static void +inject_position_context_cleanup_action (InjectPositionContext *ctx) +{ + if (ctx->timeout_id) { + g_source_remove (ctx->timeout_id); + ctx->timeout_id = 0; + } + if (ctx->indication_id) { + if (ctx->client) + g_signal_handler_disconnect (ctx->client, ctx->indication_id); + ctx->indication_id = 0; + } +} + +static void +inject_position_context_free (InjectPositionContext *ctx) +{ + inject_position_context_cleanup_action (ctx); + g_clear_object (&ctx->client); + g_slice_free (InjectPositionContext, ctx); +} + +static void +inject_position_finish_task (GTask *_task, + GError *error) +{ + InjectPositionContext *ctx; + g_autoptr(GTask) task = _task; + + ctx = g_task_get_task_data (task); + inject_position_context_cleanup_action (ctx); + if (error) + g_task_return_error (task, error); + else + g_task_return_boolean (task, TRUE); +} + +static gboolean +loc_location_inject_position_indication_timed_out (GTask *task) +{ + inject_position_finish_task (task, g_error_new (MM_CORE_ERROR, MM_CORE_ERROR_ABORTED, + "Operation timed out")); + return G_SOURCE_REMOVE; +} + +static void +loc_location_inject_position_indication_cb (QmiClientLoc *client, + QmiIndicationLocInjectPositionOutput *output, + GTask *task) +{ + QmiLocIndicationStatus status; + GError *error = NULL; + + if (!qmi_indication_loc_inject_position_output_get_indication_status (output, &status, &error)) + g_prefix_error (&error, "QMI operation failed: "); + else + mm_error_from_qmi_loc_indication_status (status, &error); + inject_position_finish_task (task, error); +} + +static void +loc_location_inject_position_ready (QmiClientLoc *client, + GAsyncResult *res, + GTask *task) +{ + g_autoptr(QmiMessageLocInjectPositionOutput) output = NULL; + GError *error = NULL; + + output = qmi_client_loc_inject_position_finish (client, res, &error); + if (!output) { + g_prefix_error (&error, "QMI operation failed: "); + inject_position_finish_task (task, error); + return; + } + + if (!qmi_message_loc_inject_position_output_get_result (output, &error)) { + inject_position_finish_task (task, error); + return; + } +} + +static void +loc_location_inject_position_req_indication_cb (QmiClientLoc *client, + QmiIndicationLocInjectPositionRequestOutput *output, + MMSharedQmi *self) +{ + g_autoptr(QmiMessageLocInjectPositionInput) input = NULL; + InjectPositionContext *ctx; + Private *priv; + GTask *task; + gdouble lat; + gdouble lon; + guint64 utc_ms = 0; + gdouble last_lat; + gdouble last_lon; + guint64 timestamp; + + qmi_indication_loc_inject_position_request_output_get_latitude (output, &lat, NULL); + qmi_indication_loc_inject_position_request_output_get_longitude (output, &lon, NULL); + qmi_indication_loc_inject_position_request_output_get_utc_timestamp (output, ×tamp, NULL); + + /* check whether modem already has position */ + if ((lat != 0.0) && (lon != 0.0) && (timestamp != 0)) + return; + + priv = get_private (MM_SHARED_QMI (self)); + mm_location_cache_get_lat_lon (priv->location_cache, &last_lat, &last_lon); + if ((last_lat == MM_LOCATION_LATITUDE_UNKNOWN) || + (last_lon == MM_LOCATION_LONGITUDE_UNKNOWN)) + return; + + task = g_task_new (self, NULL, NULL, NULL); + ctx = g_slice_new0 (InjectPositionContext); + g_task_set_task_data (task, ctx, (GDestroyNotify)inject_position_context_free); + + ctx->client = g_object_ref (client); + + input = qmi_message_loc_inject_position_input_new (); + + qmi_message_loc_inject_position_input_set_latitude (input, last_lat, NULL); + qmi_message_loc_inject_position_input_set_longitude (input, last_lon, NULL); + qmi_message_loc_inject_position_input_set_position_source (input, QMI_LOC_POSITION_SOURCE_OTHER, NULL); + + // it's not recommended to advertise accuracy better than 1000 meters to the modem + qmi_message_loc_inject_position_input_set_horizontal_uncertainty_circular (input, 1000, NULL); + qmi_message_loc_inject_position_input_set_horizontal_confidence (input, 68, NULL); + + utc_ms = g_get_real_time () / 1000; + qmi_message_loc_inject_position_input_set_utc_timestamp (input, utc_ms, NULL); + + ctx->timeout_id = g_timeout_add_seconds (10, + (GSourceFunc) loc_location_inject_position_indication_timed_out, + task); + + ctx->indication_id = g_signal_connect (client, + "inject-position", + G_CALLBACK (loc_location_inject_position_indication_cb), + task); + + qmi_client_loc_inject_position ( + QMI_CLIENT_LOC (ctx->client), + input, + 10, + NULL, /* cancellable */ + (GAsyncReadyCallback)loc_location_inject_position_ready, + task); +} + + +static void +loc_register_event_inject_req_ready (QmiClientLoc *client, + GAsyncResult *res, + GTask *task) +{ + g_autoptr(QmiMessageLocRegisterEventsOutput) output = NULL; + g_autoptr(GError) error = NULL; + MMSharedQmi *self; + Private *priv; + + output = qmi_client_loc_register_events_finish (client, res, &error); + if (!output) { + mm_obj_warn (client, "QMI operation failed: %s", error->message); + goto out; + } + + if (!qmi_message_loc_register_events_output_get_result (output, &error)) { + mm_obj_warn (client, "Couldn't not register tracking events: %s", error->message); + goto out; + } + + self = g_task_get_source_object (task); + priv = get_private (self); + + g_assert (!priv->loc_assistance_inject_time_req_indication_id); + priv->loc_assistance_inject_time_req_indication_id = + g_signal_connect (client, + "inject-time-request", + G_CALLBACK (loc_location_inject_time_req_indication_cb), + self); + + g_assert (!priv->loc_assistance_inject_position_req_indication_id); + priv->loc_assistance_inject_position_req_indication_id = + g_signal_connect (client, + "inject-position-request", + G_CALLBACK (loc_location_inject_position_req_indication_cb), + self); + +out: + g_task_return_int (task, MM_MODEM_LOCATION_ASSISTANCE_DATA_TYPE_XTRA); + g_object_unref (task); +} + static void loc_location_get_predicted_orbits_data_source_indication_cb (QmiClientLoc *client, QmiIndicationLocGetPredictedOrbitsDataSourceOutput *output, @@ -6772,13 +7135,28 @@ loc_location_get_predicted_orbits_data_source_indication_cb (QmiClientLoc } out: - if (error) + if (error) { g_task_return_error (task, error); - else if (!supported) + g_object_unref (task); + } else if (!supported) { g_task_return_int (task, MM_MODEM_LOCATION_ASSISTANCE_DATA_TYPE_NONE); - else - g_task_return_int (task, MM_MODEM_LOCATION_ASSISTANCE_DATA_TYPE_XTRA); - g_object_unref (task); + g_object_unref (task); + } else { + g_autoptr(QmiMessageLocRegisterEventsInput) input = NULL; + guint64 event_mask; + + event_mask = QMI_LOC_EVENT_REGISTRATION_FLAG_INJECT_TIME_REQUEST | + QMI_LOC_EVENT_REGISTRATION_FLAG_INJECT_POSITION_REQUEST; + input = qmi_message_loc_register_events_input_new (); + qmi_message_loc_register_events_input_set_event_registration_mask ( + input, event_mask, NULL); + qmi_client_loc_register_events (client, + input, + 10, + NULL, + (GAsyncReadyCallback) loc_register_event_inject_req_ready, + task); + } } static void diff --git a/src/tests/meson.build b/src/tests/meson.build index 18ece913..3804fe63 100644 --- a/src/tests/meson.build +++ b/src/tests/meson.build @@ -10,6 +10,7 @@ test_units = { 'charsets': libhelpers_dep, 'error-helpers': libhelpers_dep, 'kernel-device-helpers': libkerneldevice_dep, + 'location-cache': libhelpers_dep, 'modem-helpers': libhelpers_dep, 'port-scheduler': libport_dep, 'sms-part-3gpp': libhelpers_dep, diff --git a/src/tests/test-location-cache.c b/src/tests/test-location-cache.c new file mode 100644 index 00000000..fcffca0f --- /dev/null +++ b/src/tests/test-location-cache.c @@ -0,0 +1,453 @@ +/*****************************************************************************/ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This program 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. + * + * This program 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: + * + * Copyright (C) 2026 Andrey Skvortsov + */ + +#include + +#include +#include +#include + +#define _LIBMM_INSIDE_MM +#include +#include "mm-log-test.h" +#include "mm-location-cache.h" + +/*****************************************************************************/ + +static const gdouble reset_lat = 0.1; +static const gdouble reset_lon = 0.2; + +typedef struct { + const gchar *file_content; + const gdouble expect_lat; + const gdouble expect_lon; + const gboolean expect_success; +} LoadTestcase; + +static const LoadTestcase load_testcases[] = { + { + .file_content = "[last_position]\n" + "latitude=48.137\n" + "longitude=11.575\n", + .expect_lat = 48.137, + .expect_lon = 11.575, + .expect_success = TRUE, + }, + { + .file_content = "[last_position]\n" + "longitude=11.575\n" + "latitude=48.137\n", + .expect_lat = 48.137, + .expect_lon = 11.575, + .expect_success = TRUE, + }, + { + .file_content = "[last_position]\n" + "latitude=-27.142\n" + "longitude=-109.314\n", + .expect_lat = -27.142, + .expect_lon = -109.314, + .expect_success = TRUE, + }, + { + .file_content = "[last_position]\n" + "latitude=48.137\n", + .expect_lat = MM_LOCATION_LATITUDE_UNKNOWN, + .expect_lon = MM_LOCATION_LONGITUDE_UNKNOWN, + .expect_success = FALSE, + }, + { + .file_content = "[last_position]\n" + "longitude=11.575\n", + .expect_lat = MM_LOCATION_LATITUDE_UNKNOWN, + .expect_lon = MM_LOCATION_LONGITUDE_UNKNOWN, + .expect_success = FALSE, + }, + { + .file_content = "[last_position]\n", + .expect_lat = MM_LOCATION_LATITUDE_UNKNOWN, + .expect_lon = MM_LOCATION_LONGITUDE_UNKNOWN, + .expect_success = FALSE, + }, + { + .file_content = "To be, or not to be, that is the question...\n", + .expect_lat = MM_LOCATION_LATITUDE_UNKNOWN, + .expect_lon = MM_LOCATION_LONGITUDE_UNKNOWN, + .expect_success = FALSE, + }, +}; + +static void +test_location_cache_load (void) +{ + g_autoptr(MMLocationCache) cache = NULL; + gdouble get_lat; + gdouble get_lon; + g_autofree gchar *filename = NULL; + g_autoptr(GFile) loc = NULL; + g_autoptr(GIOStream) stream = NULL; + g_autoptr(GError) err = NULL; + gulong i; + gboolean ret; + + cache = mm_location_cache_new (); + g_assert_nonnull (cache); + + loc = g_file_new_tmp (NULL, (GFileIOStream **)&stream, &err); + filename = g_file_get_path (loc); + g_assert_no_error (err); + g_assert_nonnull (filename); + + mm_location_cache_set_filename (cache, filename); + + for (i = 0; i < G_N_ELEMENTS (load_testcases); i++) { + g_autoptr(GError) error = NULL; + LoadTestcase test = load_testcases[i]; + + g_debug ("load test %ld", i); + g_file_replace_contents (loc, + test.file_content, + strlen (test.file_content), + NULL, /* etag */ + FALSE, /* make backup */ + G_FILE_CREATE_NONE, /* flags */ + NULL, /* new etag */ + NULL, /* cancellable */ + &error); + g_assert_no_error (error); + + /* reset coordinates */ + mm_location_cache_update_from_lat_lon (cache, reset_lat, reset_lon); + ret = mm_location_cache_load_from_file (cache, filename, &error); + g_assert (ret == test.expect_success); + g_assert (test.expect_success ? error == NULL : error != NULL); + + mm_location_cache_get_lat_lon (cache, &get_lat, &get_lon); + g_assert_cmpfloat_with_epsilon (test.expect_lat, get_lat, 0.01); + g_assert_cmpfloat_with_epsilon (test.expect_lon, get_lon, 0.01); + } + + /* try to load missing file */ + g_file_delete (loc, NULL, NULL); + mm_location_cache_update_from_lat_lon (cache, reset_lat, reset_lon); + ret = mm_location_cache_load_from_file (cache, filename, &err); + g_assert (err != NULL); + + mm_location_cache_get_lat_lon (cache, &get_lat, &get_lon); + g_assert_cmpfloat_with_epsilon (MM_LOCATION_LATITUDE_UNKNOWN, get_lat, 0.01); + g_assert_cmpfloat_with_epsilon (MM_LOCATION_LONGITUDE_UNKNOWN, get_lon, 0.01); +} + +/*****************************************************************************/ + +typedef struct { + const gchar *nmea; + const gdouble set_lat; + const gdouble set_lon; + const gdouble expect_lat; + const gdouble expect_lon; +} UpdateTestcase; + +static const UpdateTestcase update_testcases[] = { + { + .nmea = NULL, + .set_lat = 48.137, + .set_lon = 11.575, + .expect_lat = 48.137, + .expect_lon = 11.575, + }, + { + .nmea = NULL, + .set_lat = 48.137, + .set_lon = MM_LOCATION_LONGITUDE_UNKNOWN, + .expect_lat = reset_lat, + .expect_lon = reset_lon, + }, + { + .nmea = NULL, + .set_lat = MM_LOCATION_LATITUDE_UNKNOWN, + .set_lon = 11.575, + .expect_lat = reset_lat, + .expect_lon = reset_lon, + }, + /* only GGA NMEA sentences are supported by current implementation */ + { + .nmea = "$GNGLL,4808.2200,N,01134.5000,E,012345.000,A,A*40", + .set_lat = MM_LOCATION_LATITUDE_UNKNOWN, + .set_lon = MM_LOCATION_LONGITUDE_UNKNOWN, + .expect_lat = reset_lat, + .expect_lon = reset_lon, + }, + { + .nmea = "$GNRMC,012345.000,A,4808.2200,N,01134.5000,E,1.0,300.5,100817,3.6,E,A,V*6B", + .set_lat = MM_LOCATION_LATITUDE_UNKNOWN, + .set_lon = MM_LOCATION_LONGITUDE_UNKNOWN, + .expect_lat = reset_lat, + .expect_lon = reset_lon, + }, + { + .nmea = "$GNGGA,012345.000,4808.2200,N,01134.5000,E,1,12,1.1,-14.1,M,32.0,M,,*5E", + .set_lat = MM_LOCATION_LATITUDE_UNKNOWN, + .set_lon = MM_LOCATION_LONGITUDE_UNKNOWN, + .expect_lat = 48.137, + .expect_lon = 11.575, + }, + { + .nmea = NULL, + .set_lat = -27.142, + .set_lon = -109.314, + .expect_lat = -27.142, + .expect_lon = -109.314, + }, + { + .nmea = "$GNGLL,2708.5200,S,10918.8400,W,012345.000,A,A*4E", + .set_lat = MM_LOCATION_LATITUDE_UNKNOWN, + .set_lon = MM_LOCATION_LONGITUDE_UNKNOWN, + .expect_lat = reset_lat, + .expect_lon = reset_lon, + }, + { + .nmea = "$GNRMC,012345.000,A,2708.5200,S,10918.8400,W,1.0,300.5,100817,3.6,E,A,V*65", + .set_lat = MM_LOCATION_LATITUDE_UNKNOWN, + .set_lon = MM_LOCATION_LONGITUDE_UNKNOWN, + .expect_lat = reset_lat, + .expect_lon = reset_lon, + }, + { + .nmea = "$GNGGA,012345.000,2708.5200,S,10918.8400,W,1,12,1.1,-14.1,M,32.0,M,,*50", + .set_lat = MM_LOCATION_LATITUDE_UNKNOWN, + .set_lon = MM_LOCATION_LONGITUDE_UNKNOWN, + .expect_lat = -27.142, + .expect_lon = -109.314, + }, +}; + +static void +test_location_cache_update (void) +{ + g_autoptr(MMLocationCache) cache = NULL; + gulong i; + + cache = mm_location_cache_new (); + g_assert_nonnull (cache); + + for (i = 0; i < G_N_ELEMENTS (update_testcases); i++) { + gdouble get_lat = 0; + gdouble get_lon = 0; + UpdateTestcase test = update_testcases[i]; + + g_debug ("update test %ld", i); + + /* reset coordinates */ + mm_location_cache_update_from_lat_lon (cache, reset_lat, reset_lon); + + if (test.nmea) + mm_location_cache_update_from_nmea (cache, test.nmea); + else + mm_location_cache_update_from_lat_lon (cache, test.set_lat, test.set_lon); + mm_location_cache_get_lat_lon (cache, &get_lat, &get_lon); + g_assert_cmpfloat_with_epsilon (test.expect_lat, get_lat, 0.01); + g_assert_cmpfloat_with_epsilon (test.expect_lon, get_lon, 0.01); + } +} + +/*****************************************************************************/ + +static gchar * +get_temp_filename (void) +{ + g_autoptr(GError) error = NULL; + gchar *filename; + gint fd; + + fd = g_file_open_tmp (NULL, &filename, &error); + g_assert_no_error (error); + g_assert_nonnull (filename); + + g_close (fd, &error); + g_assert_no_error (error); + + g_unlink (filename); + + return filename; +} + +static void +test_location_cache_save (void) +{ + g_autoptr(MMLocationCache) cache = NULL; + g_autoptr(GError) error = NULL; + g_autofree gchar *filename = NULL; + g_autofree gchar *path = NULL; + gboolean ret; + + cache = mm_location_cache_new (); + g_assert_nonnull (cache); + + filename = get_temp_filename (); + + // write empty cache + g_unlink (filename); + ret = mm_location_cache_save_to_file (cache, filename, &error); + g_assert_true (ret); + g_assert_no_error (error); + + ret = g_file_test (filename, G_FILE_TEST_EXISTS); + g_assert_false (ret); + + // write non-empty cache + g_unlink (filename); + mm_location_cache_update_from_lat_lon (cache, reset_lat, reset_lon); + ret = mm_location_cache_save_to_file (cache, filename, &error); + g_assert_true (ret); + g_assert_no_error (error); + + ret = g_file_test (filename, G_FILE_TEST_EXISTS); + g_assert_true (ret); + g_unlink (filename); + + // write to non-existing path + path = g_build_path (G_DIR_SEPARATOR_S, "missing", "X", "Y", "Z", "location.ini", NULL); + ret = mm_location_cache_save_to_file (cache, path, &error); + g_assert_false (ret); + g_assert (error != NULL); +} + +/*****************************************************************************/ + +typedef struct { + const gdouble set_lat; + const gdouble set_lon; +} SaveLoadTestcase; + +static const SaveLoadTestcase save_load_testcases[] = { + { + .set_lat = 48.137, + .set_lon = 11.575, + }, + { + .set_lat = -27.142, + .set_lon = -109.314, + }, +}; + +static void +test_location_cache_save_load (void) +{ + g_autoptr(MMLocationCache) cache = NULL; + g_autoptr(GError) error = NULL; + g_autofree gchar *filename = NULL; + gboolean ret; + gulong i; + + cache = mm_location_cache_new (); + g_assert_nonnull (cache); + + filename = get_temp_filename (); + mm_location_cache_set_filename (cache, filename); + + for (i = 0; i < G_N_ELEMENTS (save_load_testcases); i++) { + gdouble get_lat = 0; + gdouble get_lon = 0; + SaveLoadTestcase test = save_load_testcases[i]; + + g_debug ("save/load test %ld", i); + + mm_location_cache_update_from_lat_lon (cache, test.set_lat, test.set_lon); + g_unlink (filename); + ret = mm_location_cache_save (cache, &error); + g_assert_true (ret); + g_assert_no_error (error); + + ret = g_file_test (filename, G_FILE_TEST_EXISTS); + g_assert_true (ret); + + mm_location_cache_update_from_lat_lon (cache, reset_lat, reset_lon); + ret = mm_location_cache_load (cache, &error); + g_assert_true (ret); + g_assert_no_error (error); + + mm_location_cache_get_lat_lon (cache, &get_lat, &get_lon); + g_assert_cmpfloat_with_epsilon (test.set_lat, get_lat, 0.01); + g_assert_cmpfloat_with_epsilon (test.set_lon, get_lon, 0.01); + } + + g_unlink (filename); +} + +/*****************************************************************************/ + +static void +test_location_cache_create_dispose (void) +{ + MMLocationCache *cache = NULL; + g_autoptr(GError) error = NULL; + g_autofree gchar *filename = NULL; + gboolean ret; + gdouble get_lat = 0; + gdouble get_lon = 0; + + cache = mm_location_cache_new (); + g_assert_nonnull (cache); + + // to check later, that MMLocationCache object is finalized + g_object_add_weak_pointer (G_OBJECT (cache), (gpointer *) &cache); + + filename = get_temp_filename (); + mm_location_cache_set_filename (cache, filename); + mm_location_cache_update_from_lat_lon (cache, reset_lat, reset_lon); + + // drop the last reference and save cached location + g_object_unref (cache); + g_assert_null (cache); + + // check whether file is created + ret = g_file_test (filename, G_FILE_TEST_EXISTS); + g_assert_true (ret); + + // check file content + cache = mm_location_cache_new (); + g_assert_nonnull (cache); + + mm_location_cache_set_filename (cache, filename); + ret = mm_location_cache_load (cache, &error); + g_assert_true (ret); + g_assert_no_error (error); + + mm_location_cache_get_lat_lon (cache, &get_lat, &get_lon); + g_assert_cmpfloat_with_epsilon (reset_lat, get_lat, 0.01); + g_assert_cmpfloat_with_epsilon (reset_lon, get_lon, 0.01); + + g_object_unref (cache); + g_unlink (filename); +} + +/*****************************************************************************/ + +int main (int argc, char **argv) +{ + setlocale (LC_ALL, ""); + + g_test_init (&argc, &argv, NULL); + + g_test_add_func ("/MM/location-cache/update", test_location_cache_update); + g_test_add_func ("/MM/location-cache/load", test_location_cache_load); + g_test_add_func ("/MM/location-cache/save", test_location_cache_save); + g_test_add_func ("/MM/location-cache/save_load", test_location_cache_save_load); + g_test_add_func ("/MM/location-cache/create_dispose", test_location_cache_create_dispose); + + return g_test_run (); +}