service-providers: Add parsing for service provider info

We currently only need the emergency number information

Signed-off-by: Guido Günther <agx@sigxcpu.org>
Part-of: <https://gitlab.gnome.org/GNOME/calls/-/merge_requests/790>
This commit is contained in:
Guido Günther
2025-09-07 17:43:28 +02:00
committed by Marge Bot
parent 5d751f3143
commit 07d751f958
7 changed files with 647 additions and 1 deletions

View File

@@ -25,7 +25,7 @@ typedef enum {
/* See 3GPP TS 22.101 version 14.8.0 Release 14, Chapter 10.1 */
typedef enum {
CALLS_EMERGENCY_CALL_TYPE_UNKNOWN = 0,
CALLS_EMERGENCY_CALL_TYPE_NONE = 0,
CALLS_EMERGENCY_CALL_TYPE_POLICE = (1 << 0),
CALLS_EMERGENCY_CALL_TYPE_AMBULANCE = (1 << 1),
CALLS_EMERGENCY_CALL_TYPE_FIRE_BRIGADE = (1 << 2),

View File

@@ -0,0 +1,461 @@
/*
* Copyright (C) 2025 The Phosh.mobi e.V.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Author: Guido Günther <agx@sigxcpu.org>
*/
#define G_LOG_DOMAIN "calls-service-providers"
#include "calls-emergency-call-types.h"
#include "calls-service-providers.h"
#include <gio/gio.h>
#include <gmobile.h>
typedef enum {
PARSER_TOPLEVEL = 0,
PARSER_COUNTRY,
PARSER_EMERGENCY_NUMBERS,
PARSER_EMERGENCY_NUMBER,
PARSER_CALLEE,
PARSER_DONE,
PARSER_ERROR
} GsdParseContextState;
typedef struct {
GMarkupParseContext *ctx;
GHashTable *info;
char buffer[4096];
char *text_buffer;
GsdParseContextState state;
CallsEmergencyCallCountryData *current_data;
CallsEmergencyNumber *current_number;
CallsEmergencyCallTypeFlags current_flags;
} CallsParseContext;
typedef struct {
GAsyncResult *res;
GMainLoop *loop;
} GetChannelsSyncData;
static void
calls_parse_context_free (CallsParseContext *parse_context)
{
g_markup_parse_context_free (parse_context->ctx);
g_clear_pointer (&parse_context->current_data, calls_emergency_call_country_data_free);
g_clear_pointer (&parse_context->current_number, calls_emergency_number_free);
g_clear_pointer (&parse_context->info, g_hash_table_unref);
g_clear_pointer (&parse_context->text_buffer, g_free);
g_free (parse_context);
}
static void
parser_toplevel_start (CallsParseContext *parse_context,
const char *name,
const char **attribute_names,
const char **attribute_values)
{
if (g_str_equal (name, "serviceproviders")) {
for (int i = 0; !gm_str_is_null_or_empty (attribute_names[i]); i++) {
if (g_str_equal (attribute_names[i], "format")) {
if (!g_str_equal (attribute_values[i], "2.0")) {
g_warning ("Mobile broadband provider database format '%s' not supported.",
attribute_values[i]);
parse_context->state = PARSER_ERROR;
break;
}
}
}
} else if (g_str_equal (name, "country")) {
parse_context->state = PARSER_COUNTRY;
if (parse_context->current_data) {
g_warning ("Country '%s' not fully parsed", parse_context->current_data->country_code);
g_clear_pointer (&parse_context->current_data, calls_emergency_call_country_data_free);
}
for (int i = 0; !gm_str_is_null_or_empty (attribute_names[i]); i++) {
if (g_str_equal (attribute_names[i], "code")) {
g_assert (parse_context->current_data == NULL);
parse_context->current_data = calls_emergency_call_country_data_new (attribute_values[i]);
break;
}
}
}
}
static void
parser_country_start (CallsParseContext *parse_context,
const char *name,
const char **attribute_names,
const char **attribute_values)
{
if (g_str_equal (name, "emergency-numbers"))
parse_context->state = PARSER_EMERGENCY_NUMBERS;
}
static void
parser_emergency_numbers_start (CallsParseContext *parse_context,
const char *name,
const char **attribute_names,
const char **attribute_values)
{
if (g_str_equal (name, "emergency-number")) {
for (int i = 0; !gm_str_is_null_or_empty (attribute_names[i]); i++) {
if (g_str_equal (attribute_names[i], "number")) {
g_assert (parse_context->current_number == NULL);
parse_context->current_number = calls_emergency_number_new (attribute_values[i],
CALLS_EMERGENCY_CALL_TYPE_NONE);
break;
}
}
parse_context->state = PARSER_EMERGENCY_NUMBER;
}
}
static CallsEmergencyCallTypeFlags
type_to_flag (const char *type)
{
if (g_str_equal (type, "police")) {
return CALLS_EMERGENCY_CALL_TYPE_POLICE;
} else if (g_str_equal (type, "ambulance")) {
return CALLS_EMERGENCY_CALL_TYPE_AMBULANCE;
} else if (g_str_equal (type, "fire-brigade")) {
return CALLS_EMERGENCY_CALL_TYPE_FIRE_BRIGADE;
}
return CALLS_EMERGENCY_CALL_TYPE_NONE;
}
static void
parser_emergency_number_start (CallsParseContext *parse_context,
const char *name,
const char **attribute_names,
const char **attribute_values)
{
if (g_str_equal (name, "callee")) {
g_assert (parse_context->current_flags == CALLS_EMERGENCY_CALL_TYPE_NONE);
for (int i = 0; !gm_str_is_null_or_empty (attribute_names[i]); i++) {
if (g_str_equal (attribute_names[i], "type")) {
parse_context->current_flags = type_to_flag (attribute_values[i]);
break;
}
}
parse_context->state = PARSER_CALLEE;
}
}
static void
parser_start_element (GMarkupParseContext *context,
const char *element_name,
const char **attribute_names,
const char **attribute_values,
gpointer user_data,
GError **error)
{
CallsParseContext *parse_context = user_data;
g_clear_pointer (&parse_context->text_buffer, g_free);
switch (parse_context->state) {
case PARSER_TOPLEVEL:
parser_toplevel_start (parse_context, element_name, attribute_names, attribute_values);
break;
case PARSER_COUNTRY:
parser_country_start (parse_context, element_name, attribute_names, attribute_values);
break;
case PARSER_EMERGENCY_NUMBERS:
parser_emergency_numbers_start (parse_context, element_name, attribute_names, attribute_values);
break;
case PARSER_EMERGENCY_NUMBER:
parser_emergency_number_start (parse_context, element_name, attribute_names, attribute_values);
break;
case PARSER_CALLEE:
break;
case PARSER_ERROR:
break;
case PARSER_DONE:
break;
default:
g_assert_not_reached ();
}
}
static void
parser_callee_end (CallsParseContext *parse_context, const char *name)
{
if (g_str_equal (name, "callee")) {
parse_context->current_number->flags |= parse_context->current_flags;
parse_context->current_flags = CALLS_EMERGENCY_CALL_TYPE_NONE;
g_clear_pointer (&parse_context->text_buffer, g_free);
parse_context->state = PARSER_EMERGENCY_NUMBER;
}
}
static void
parser_emergency_number_end (CallsParseContext *parse_context, const char *name)
{
if (g_str_equal (name, "emergency-number")) {
g_ptr_array_add (parse_context->current_data->numbers,
g_steal_pointer (&parse_context->current_number));
g_clear_pointer (&parse_context->text_buffer, g_free);
parse_context->state = PARSER_EMERGENCY_NUMBERS;
}
}
static void
parser_emergency_numbers_end (CallsParseContext *parse_context, const char *name)
{
if (g_str_equal (name, "emergency-numbers")) {
g_clear_pointer (&parse_context->text_buffer, g_free);
parse_context->state = PARSER_COUNTRY;
}
}
static void
parser_country_end (CallsParseContext *parse_context, const char *name)
{
if (g_str_equal (name, "country")) {
/* Only add country if we have any emergency numbers */
if (parse_context->current_data->numbers->len) {
g_hash_table_insert (parse_context->info,
parse_context->current_data->country_code,
parse_context->current_data);
parse_context->current_data = NULL;
}
g_clear_pointer (&parse_context->current_data, calls_emergency_call_country_data_free);
g_clear_pointer (&parse_context->text_buffer, g_free);
parse_context->state = PARSER_TOPLEVEL;
}
}
static void
parser_end_element (GMarkupParseContext *context,
const char *element_name,
gpointer user_data,
GError **error)
{
CallsParseContext *parse_context = user_data;
switch (parse_context->state) {
case PARSER_TOPLEVEL:
break;
case PARSER_COUNTRY:
parser_country_end (parse_context, element_name);
break;
case PARSER_EMERGENCY_NUMBERS:
parser_emergency_numbers_end (parse_context, element_name);
break;
case PARSER_EMERGENCY_NUMBER:
parser_emergency_number_end (parse_context, element_name);
break;
case PARSER_CALLEE:
parser_callee_end (parse_context, element_name);
case PARSER_ERROR:
break;
case PARSER_DONE:
break;
default:
g_assert_not_reached ();
}
}
static void
parser_text (GMarkupParseContext *context,
const char *text,
gsize text_len,
gpointer user_data,
GError **error)
{
CallsParseContext *parse_context = user_data;
g_free (parse_context->text_buffer);
parse_context->text_buffer = g_strdup (text);
}
static const GMarkupParser parser = {
.start_element = parser_start_element,
.end_element = parser_end_element,
.text = parser_text,
.passthrough = NULL,
.error = NULL,
};
static void read_next_chunk (GInputStream *stream, GTask *task);
static void
on_stream_read_ready (GObject *source_object, GAsyncResult *res, gpointer user_data)
{
GInputStream *stream = G_INPUT_STREAM (source_object);
g_autoptr (GTask) task = G_TASK (user_data);
CallsParseContext *parse_context = g_task_get_task_data (task);
gssize len;
GError *error = NULL;
len = g_input_stream_read_finish (stream, res, &error);
if (len == -1) {
g_prefix_error (&error, "Error reading service provider database: ");
g_task_return_error (task, error);
return;
}
if (len == 0) {
g_task_return_pointer (task,
g_steal_pointer (&parse_context->info),
(GDestroyNotify)g_hash_table_unref);
return;
}
if (!g_markup_parse_context_parse (parse_context->ctx, parse_context->buffer, len, &error)) {
g_prefix_error (&error, "Error parsing service provider database: ");
g_task_return_error (task, error);
return;
}
read_next_chunk (stream, g_steal_pointer (&task));
}
static void
read_next_chunk (GInputStream *stream, GTask *task)
{
CallsParseContext *parse_context = g_task_get_task_data (task);
g_input_stream_read_async (stream,
parse_context->buffer,
sizeof (parse_context->buffer),
G_PRIORITY_DEFAULT,
g_task_get_cancellable (task),
on_stream_read_ready,
task);
}
static void
on_file_read_ready (GObject *source_object, GAsyncResult *res, gpointer user_data)
{
g_autoptr (GTask) task = G_TASK (user_data);
g_autoptr (GFileInputStream) stream = NULL;
GError *error = NULL;
GFile *file = G_FILE (source_object);
stream = g_file_read_finish (file, res, &error);
if (!stream) {
g_prefix_error (&error, "Error opening service provider database: ");
g_task_return_error (task, error);
return;
}
read_next_chunk (G_INPUT_STREAM (stream), g_steal_pointer (&task));
}
GHashTable *
calls_service_providers_get_emergency_info_finish (GAsyncResult *res,
GError **error)
{
g_assert (G_IS_TASK (res));
g_assert (g_task_get_source_tag(G_TASK (res)) == calls_service_providers_get_emergency_info);
return g_task_propagate_pointer (G_TASK (res), error);
}
void
calls_service_providers_get_emergency_info (const char *serviceproviders,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data)
{
g_autoptr (GFile) file = NULL;
g_autoptr (GTask) task = g_task_new (NULL, cancellable, callback, user_data);
CallsParseContext *parse_context = g_new0 (CallsParseContext, 1);
g_assert (serviceproviders);
parse_context->ctx = g_markup_parse_context_new (&parser, 0, parse_context, NULL);
parse_context->info = g_hash_table_new_full (g_str_hash,
g_str_equal,
NULL,
(GDestroyNotify)
calls_emergency_call_country_data_free);
g_task_set_task_data (task, parse_context, (GDestroyNotify)calls_parse_context_free);
g_task_set_source_tag (task, calls_service_providers_get_emergency_info);
file = g_file_new_for_path (serviceproviders);
g_file_read_async (file, G_PRIORITY_DEFAULT,
cancellable,
on_file_read_ready,
g_steal_pointer (&task));
}
static void
on_get_emergency_info_ready (GObject *object, GAsyncResult *res, gpointer user_data)
{
GetChannelsSyncData *data = user_data;
g_assert (data->res == NULL);
data->res = g_object_ref (res);
g_main_loop_quit (data->loop);
}
GHashTable *
calls_service_providers_get_emergency_info_sync (const char *serviceproviders,
GError **error)
{
GHashTable *info;
GetChannelsSyncData data;
g_autoptr (GMainContext) context = g_main_context_new ();
g_autoptr (GMainLoop) loop = NULL;
g_main_context_push_thread_default (context);
loop = g_main_loop_new (context, FALSE);
data = (GetChannelsSyncData) {
.loop = loop,
.res = NULL,
};
calls_service_providers_get_emergency_info (serviceproviders,
NULL,
on_get_emergency_info_ready,
&data);
g_main_loop_run (data.loop);
info = calls_service_providers_get_emergency_info_finish (data.res, error);
g_clear_object (&data.res);
g_main_context_pop_thread_default (context);
return info;
}

View File

@@ -0,0 +1,23 @@
/*
* Copyright (C) 2025 The Phosh.mobi e.V.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
#pragma once
#include <gio/gio.h>
G_BEGIN_DECLS
void calls_service_providers_get_emergency_info (const char *serviceproviders,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data);
GHashTable *calls_service_providers_get_emergency_info_finish (GAsyncResult *res,
GError **error);
GHashTable *calls_service_providers_get_emergency_info_sync (const char *serviceproviders,
GError **error) G_GNUC_WARN_UNUSED_RESULT;
G_END_DECLS

View File

@@ -123,6 +123,7 @@ calls_sources = files([
'calls-ringer.c', 'calls-ringer.h',
'calls-secret-store.c', 'calls-secret-store.h',
'calls-settings.c', 'calls-settings.h',
'calls-service-providers.c', 'calls-service-providers.h',
'calls-ui-call-data.c', 'calls-ui-call-data.h',
'calls-ussd.c', 'calls-ussd.h',
'calls-util.c', 'calls-util.h',

View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding='utf-8'?>
<!-- -*- Mode: XML; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- -->
<!DOCTYPE serviceproviders SYSTEM "serviceproviders.2.dtd">
<serviceproviders format="2.0">
<country code="yy">
<name>Germany</name>
<emergency-numbers>
<emergency-number number="112">
<callee type="ambulance"/>
<callee type="fire-brigade"/>
<callee type="police"/>
</emergency-number>
</emergency-numbers>
<provider>
</provider>
</country>
<country code="xx">
<name>Switzerland</name>
<emergency-numbers>
<emergency-number number="114">
<callee type="ambulance"/>
</emergency-number>
<emergency-number number="117">
<callee type="police"/>
</emergency-number>
<emergency-number number="118">
<callee type="fire-brigade"/>
</emergency-number>
</emergency-numbers>
</country>
<country code="zz">
</country>
</serviceproviders>

View File

@@ -19,6 +19,7 @@ test_env = [
test_cflags = [
'-DFOR_TESTING',
'-Wno-error=deprecated-declarations',
'-DTEST_DATABASE="@0@"'.format(meson.current_source_dir() / 'data' / 'serviceproviders.xml'),
]
test_link_args = [
@@ -52,6 +53,19 @@ t = executable('emergency-call-types', test_sources,
)
test('emergency-call-types', t, env: test_env)
test_sources = [ 'test-service-providers.c' ]
t = executable('service-providers', test_sources,
c_args : test_cflags,
link_args: test_link_args,
pie: true,
link_with : [calls_vala, libcalls],
dependencies: calls_deps,
include_directories : [
calls_includes,
]
)
test('service-providers', t, env: test_env)
test_sources = [ 'test-manager.c' ]
t = executable('manager', test_sources,

View File

@@ -0,0 +1,112 @@
/*
* Copyright (C) 2025 The Phosh.mobi e.V.
*
* SPDX-License-Identifier: GPL-3.0+
*
* Author: Guido Günther <agx@sigxcpu.org>
*/
#include "calls-emergency-call-types.h"
#include "calls-service-providers.h"
#include <gio/gio.h>
#include <glib.h>
#define calls_assert_cmp_emergency_number(d, i, n, f) G_STMT_START { \
CallsEmergencyNumber *_n = g_ptr_array_index (d->numbers, i); \
if (!_n) { \
g_autofree char *__msg = \
g_strdup_printf ("Emergency number '%u' does not exist", i); \
g_assertion_message (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, __msg); \
} \
if (!_n->number) { \
g_autofree char *__msg = \
g_strdup_printf ("Emergency number of element '%u' is NULL", i); \
g_assertion_message (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, __msg); \
} \
if (!g_str_equal (_n->number, n)) { \
g_autofree char *__msg = \
g_strdup_printf ("Emergency number of element '%u' is '%s' not '%s'", i, _n->number, n); \
g_assertion_message (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, __msg); \
} \
if (_n->flags != f) { \
g_autofree char *__msg = \
g_strdup_printf ("Emergency number of element '%u' has flags '0x%x'' not '0x%x'", i, _n->flags, f); \
g_assertion_message (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, __msg); \
} \
} G_STMT_END
static gboolean
numbers_equal (gconstpointer a, gconstpointer b)
{
const CallsEmergencyNumber *n_a = a;
const char *needle = b;
return g_str_equal (n_a->number, needle);
}
static void
test_service_providers_parse_de (void)
{
g_autoptr (GHashTable) info = NULL;
g_autoptr (GError) err = NULL;
CallsEmergencyCallCountryData *data;
CallsEmergencyNumber *number;
guint index;
info = g_hash_table_new_full (g_str_hash,
g_str_equal,
NULL,
(GDestroyNotify) calls_emergency_call_country_data_free);
info = calls_service_providers_get_emergency_info_sync (TEST_DATABASE, &err);
g_assert_no_error (err);
g_assert_nonnull (info);
data = g_hash_table_lookup (info, "xx");
g_assert_nonnull (data);
g_assert_cmpint (data->numbers->len, ==, 3);
g_assert_true (g_ptr_array_find_with_equal_func (data->numbers, "114", numbers_equal, &index));
number = g_ptr_array_index (data->numbers, index);
g_assert_nonnull (number);
calls_assert_cmp_emergency_number(data, index, "114", CALLS_EMERGENCY_CALL_TYPE_AMBULANCE);
g_assert_true (g_ptr_array_find_with_equal_func (data->numbers, "117", numbers_equal, &index));
number = g_ptr_array_index (data->numbers, index);
g_assert_nonnull (number);
calls_assert_cmp_emergency_number(data, index, "117", CALLS_EMERGENCY_CALL_TYPE_POLICE);
g_assert_true (g_ptr_array_find_with_equal_func (data->numbers, "118", numbers_equal, &index));
number = g_ptr_array_index (data->numbers, index);
g_assert_nonnull (number);
calls_assert_cmp_emergency_number(data, index, "118", CALLS_EMERGENCY_CALL_TYPE_FIRE_BRIGADE);
data = g_hash_table_lookup (info, "yy");
g_assert_nonnull (data);
g_assert_cmpint (data->numbers->len, ==, 1);
g_assert_true (g_ptr_array_find_with_equal_func (data->numbers, "112", numbers_equal, &index));
number = g_ptr_array_index (data->numbers, index);
g_assert_nonnull (number);
calls_assert_cmp_emergency_number(data, index, "112", (CALLS_EMERGENCY_CALL_TYPE_POLICE |
CALLS_EMERGENCY_CALL_TYPE_FIRE_BRIGADE |
CALLS_EMERGENCY_CALL_TYPE_AMBULANCE));
data = g_hash_table_lookup (info, "zz");
g_assert_null (data);
}
gint
main (gint argc, gchar *argv[])
{
g_test_init (&argc, &argv, NULL);
g_test_add_func ("/calls/service-providers/parse", test_service_providers_parse_de);
return g_test_run ();
}