1
0
Fork 0
mirror of https://gitlab.gnome.org/GNOME/calls.git synced 2024-05-14 09:09:28 +00:00
Purism-Calls/src/calls-record-store.c
Evangelos Ribeiro Tzaras fb07e0122d record-store: Emit signal once DB is migrated
This helps letting CallsApplication decide if it should run a few more
iterations to ensure any critical code finishes executing.
2023-02-09 19:07:32 +00:00

793 lines
20 KiB
C

/*
* Copyright (C) 2018, 2019 Purism SPC
*
* This file is part of Calls.
*
* Calls 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 3 of the License, or
* (at your option) any later version.
*
* Calls 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 Calls. If not, see <http://www.gnu.org/licenses/>.
*
* Author: Bob Ham <bob.ham@puri.sm>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
*/
#define G_LOG_DOMAIN "CallsRecordStore"
#include "calls-config.h"
#include "calls-call-record.h"
#include "calls-manager.h"
#include "calls-ui-call-data.h"
#include "calls-record-store.h"
#include <gom/gom.h>
#include <glib/gi18n.h>
#include <glib/gstdio.h>
#include <errno.h>
#define RECORD_STORE_FILENAME "records.db"
#define RECORD_STORE_VERSION 2
enum {
SIGNAL_DB_DONE,
N_SIGNALS
};
static guint signals[N_SIGNALS];
typedef enum {
STARTED,
ANSWERED,
ENDED
} CallsCallRecordState;
static CallsCallRecordState
state_to_record_state (CuiCallState call_state)
{
switch (call_state) {
case CUI_CALL_STATE_CALLING:
case CUI_CALL_STATE_INCOMING:
return STARTED;
case CUI_CALL_STATE_ACTIVE:
case CUI_CALL_STATE_HELD:
return ANSWERED;
case CUI_CALL_STATE_DISCONNECTED:
return ENDED;
default:
g_assert_not_reached ();
}
}
static const char *
record_state_to_string (CallsCallRecordState state)
{
switch (state) {
case STARTED:
return "started";
case ANSWERED:
return "answered";
case ENDED:
return "ended";
default:
return "unknown";
}
}
struct _CallsRecordStore {
GtkApplicationWindow parent_instance;
gchar *filename;
GomAdapter *adapter;
GomRepository *repository;
};
G_DEFINE_TYPE (CallsRecordStore, calls_record_store, G_TYPE_LIST_STORE);
static void
delete_record_cb (GomResource *resource,
GAsyncResult *res,
CallsRecordStore *self)
{
g_autoptr (GError) error = NULL;
gboolean ok;
guint id;
ok = gom_resource_delete_finish (resource,
res,
&error);
g_object_get (G_OBJECT (resource),
"id",
&id,
NULL);
if (!ok) {
if (error) {
g_warning ("Error deleting call record with id %u from database %s",
id, error->message);
return;
} else {
g_warning ("Unknown error deleting call record with id %u from database",
id);
}
} else {
g_debug ("Successfully deleted call record with id %u from database",
id);
}
}
static void
delete_call_cb (CallsCallRecord *record,
CallsRecordStore *self)
{
gom_resource_delete_async (GOM_RESOURCE (record),
(GAsyncReadyCallback) delete_record_cb,
self);
}
static void
load_calls_fetch_cb (GomResourceGroup *group,
GAsyncResult *res,
CallsRecordStore *self)
{
g_autoptr (GError) error = NULL;
gboolean ok;
guint count, i;
gpointer *records;
ok = gom_resource_group_fetch_finish (group,
res,
&error);
if (error) {
g_debug ("Error fetching call records: %s",
error->message);
goto exit;
return;
}
g_assert (ok);
count = gom_resource_group_get_count (group);
g_debug ("Fetched %u call records from database `%s'",
count, self->filename);
records = g_new (gpointer, count);
for (i = 0; i < count; ++i) {
GomResource *resource;
CallsCallRecord *record;
resource = gom_resource_group_get_index (group, i);
g_assert (resource != NULL);
g_assert (CALLS_IS_CALL_RECORD (resource));
record = CALLS_CALL_RECORD (resource);
records[i] = record;
g_signal_connect (record,
"call-delete",
G_CALLBACK (delete_call_cb),
self);
}
g_list_store_splice (G_LIST_STORE (self),
0,
0,
records,
count);
g_free (records);
g_object_unref (group);
exit:
g_object_unref (self);
}
static void
load_calls_find_cb (GomRepository *repository,
GAsyncResult *res,
CallsRecordStore *self)
{
g_autoptr (GError) error = NULL;
GomResourceGroup *group;
guint count;
group = gom_repository_find_finish (repository,
res,
&error);
if (error) {
g_debug ("Error finding call records in database `%s': %s",
self->filename, error->message);
goto exit;
}
g_assert (group != NULL);
count = gom_resource_group_get_count (group);
if (count == 0) {
g_debug ("No call records found in database `%s'",
self->filename);
goto exit;
}
g_debug ("Found %u call records in database `%s', fetching",
count, self->filename);
gom_resource_group_fetch_async (group,
0,
count,
(GAsyncReadyCallback) load_calls_fetch_cb,
g_object_ref (self));
exit:
g_object_unref (self);
}
static void
load_calls (CallsRecordStore *self)
{
GomFilter *filter;
GomSorting *sorting;
filter = gom_filter_new_is_not_null
(CALLS_TYPE_CALL_RECORD, "start");
sorting = gom_sorting_new (CALLS_TYPE_CALL_RECORD,
"start",
GOM_SORTING_DESCENDING,
NULL);
g_debug ("Finding records in call record database `%s'",
self->filename);
gom_repository_find_sorted_async (self->repository,
CALLS_TYPE_CALL_RECORD,
filter,
sorting,
(GAsyncReadyCallback) load_calls_find_cb,
g_object_ref (self));
g_object_unref (G_OBJECT (filter));
}
static void
set_up_repo_migrate_cb (GomRepository *repo,
GAsyncResult *res,
CallsRecordStore *self)
{
g_autoptr (GError) error = NULL;
gboolean ok;
ok = gom_repository_automatic_migrate_finish (repo, res, &error);
if (!ok) {
if (error)
g_warning ("Error migrating call record database `%s': %s",
self->filename, error->message);
else
g_warning ("Unknown error migrating call record database `%s'",
self->filename);
g_clear_object (&self->repository);
g_clear_object (&self->adapter);
} else {
g_debug ("Successfully migrated call record database `%s'",
self->filename);
load_calls (self);
}
g_signal_emit (self, signals[SIGNAL_DB_DONE], 0, ok);
g_object_unref (self);
}
static void
set_up_repo (CallsRecordStore *self)
{
GomRepository *repo;
GList *types = NULL;
if (self->repository) {
g_warning ("Opened call record database `%s'"
" while repository exists",
self->filename);
return;
}
repo = gom_repository_new (self->adapter);
g_debug ("Attempting migration of call"
" record database `%s'",
self->filename);
types = g_list_append (types, (gpointer) CALLS_TYPE_CALL_RECORD);
gom_repository_automatic_migrate_async
(repo,
RECORD_STORE_VERSION,
types,
(GAsyncReadyCallback) set_up_repo_migrate_cb,
g_object_ref (self));
self->repository = repo;
}
static void
close_adapter (CallsRecordStore *self)
{
g_autoptr (GError) error = NULL;
gboolean ok;
if (!self->adapter) {
return;
}
ok = gom_adapter_close_sync (self->adapter, &error);
if (!ok) {
if (error)
g_warning ("Error closing call record database `%s': %s",
self->filename, error->message);
else
g_warning ("Unknown error closing call record database `%s'",
self->filename);
}
g_clear_object (&self->adapter);
}
static void
open_repo_adapter_open_cb (GomAdapter *adapter,
GAsyncResult *res,
CallsRecordStore *self)
{
g_autoptr (GError) error = NULL;
gboolean ok;
ok = gom_adapter_open_finish (adapter, res, &error);
if (!ok) {
if (error)
g_warning ("Error opening call record database `%s': %s",
self->filename, error->message);
else
g_warning ("Unknown error opening call record database `%s'",
self->filename);
close_adapter (self);
g_signal_emit (self, signals[SIGNAL_DB_DONE], 0, FALSE);
} else {
g_debug ("Successfully opened call record database `%s'",
self->filename);
set_up_repo (self);
}
g_object_unref (self);
}
static void
open_repo (CallsRecordStore *self)
{
gchar *dir;
gint err;
gchar *uri;
if (self->adapter)
return;
dir = g_path_get_dirname (self->filename);
err = g_mkdir_with_parents (dir, 0755);
if (err)
g_warning ("Could not create Calls data directory `%s': %s",
dir, g_strerror (errno));
g_free (dir);
uri = g_strdup_printf ("file:%s", self->filename);
g_debug ("Opening call record database using URI `%s'", uri);
self->adapter = gom_adapter_new ();
gom_adapter_open_async
(self->adapter,
uri,
(GAsyncReadyCallback) open_repo_adapter_open_cb,
g_object_ref (self));
g_free (uri);
}
struct CallsRecordCallData {
CallsRecordStore *self;
CallsUiCallData *call;
};
static void
record_call_save_cb (GomResource *resource,
GAsyncResult *res,
struct CallsRecordCallData *data)
{
GObject * const call_obj = G_OBJECT (data->call);
g_autoptr (GError) error = NULL;
gboolean ok;
ok = gom_resource_save_finish (resource, res, &error);
if (!ok) {
if (error)
g_warning ("Error saving call record to database: %s",
error->message);
else
g_warning ("Unknown error saving call record to database");
g_object_set_data (call_obj, "calls-call-record", NULL);
} else {
g_debug ("Successfully saved new call record to database");
g_list_store_insert (G_LIST_STORE (data->self),
0,
CALLS_CALL_RECORD (resource));
g_object_set_data (call_obj, "calls-call-start", NULL);
}
g_object_unref (data->call);
g_object_unref (data->self);
g_free (data);
}
static void
record_call (CallsRecordStore *self,
CallsUiCallData *call)
{
GObject * const call_obj = G_OBJECT (call);
GDateTime *start;
CallsCallRecord *record;
struct CallsRecordCallData *data;
gboolean inbound;
g_autofree char *protocol = NULL;
g_assert (g_object_get_data (call_obj, "calls-call-record") == NULL);
start = g_object_get_data (call_obj, "calls-call-start");
g_assert (start != NULL);
g_object_get (call, "inbound", &inbound, "protocol", &protocol, NULL);
record = g_object_new (CALLS_TYPE_CALL_RECORD,
"repository", self->repository,
"target", cui_call_get_id (CUI_CALL (call)),
"inbound", inbound,
"protocol", protocol,
"start", start,
NULL);
g_object_set_data_full (call_obj, "calls-call-record",
record, g_object_unref);
data = g_new (struct CallsRecordCallData, 1);
g_object_ref (self);
g_object_ref (call);
data->self = self;
data->call = call;
gom_resource_save_async (GOM_RESOURCE (record),
(GAsyncReadyCallback) record_call_save_cb,
data);
}
static void
update_cb (GomResource *resource,
GAsyncResult *res,
gpointer *unused)
{
g_autoptr (GError) error = NULL;
gboolean ok;
ok = gom_resource_save_finish (resource, res, &error);
if (!ok) {
if (error)
g_warning ("Error updating call record in database: %s",
error->message);
else
g_warning ("Unknown error updating call record in database");
} else {
g_debug ("Successfully updated call record in database");
}
}
static void
stamp_call (CallsCallRecord *record,
const gchar *stamp_name)
{
GObject *record_obj = G_OBJECT (record);
GDateTime *stamp = NULL;
/* Check the call has not already been stamped */
g_object_get (record_obj,
stamp_name, &stamp,
NULL);
if (stamp)
return;
g_debug ("Stamping call `%s'", stamp_name);
stamp = g_date_time_new_now_utc ();
g_object_set (record_obj,
stamp_name, stamp,
NULL);
g_date_time_unref (stamp);
gom_resource_save_async (GOM_RESOURCE (record),
(GAsyncReadyCallback) update_cb,
NULL);
}
static void
state_changed_cb (CallsRecordStore *self,
CuiCallState new_state,
CuiCallState old_state,
CallsUiCallData *call)
{
GObject *call_obj = G_OBJECT (call);
CallsCallRecord *record =
g_object_get_data (call_obj, "calls-call-record");
CallsCallRecordState new_rec_state, old_rec_state;
g_debug ("Call state changed from %s (%d) to %s (%d)",
cui_call_state_to_string (old_state), old_state,
cui_call_state_to_string (new_state), new_state);
/* Check whether the call is recorded */
if (!record) {
/* Try to record the call again */
if (g_object_get_data (call_obj, "calls-call-start") != NULL)
record_call (self, call);
else
g_warning ("Record store received state change"
" for non-started call");
return;
}
new_rec_state = state_to_record_state (new_state);
old_rec_state = state_to_record_state (old_state);
if (new_rec_state == old_rec_state)
return;
switch (old_rec_state) {
case STARTED:
switch (new_rec_state) {
case ANSWERED:
stamp_call (record, "answered");
break;
case ENDED:
stamp_call (record, "end");
break;
case STARTED:
default:
goto critical_exit;
}
break;
case ANSWERED:
switch (new_rec_state) {
case ENDED:
stamp_call (record, "end");
break;
case STARTED:
case ANSWERED:
default:
goto critical_exit;
}
break;
case ENDED:
default:
goto critical_exit;
}
return;
critical_exit:
/* XXX Modem's may be buggy; let's print as much information as possible */
g_critical ("Unexpected state change occurred!\n"
"From %s (%s) to %s (%s)",
cui_call_state_to_string (old_state), record_state_to_string (old_rec_state),
cui_call_state_to_string (new_state), record_state_to_string (new_rec_state));
}
static void
call_added_cb (CallsRecordStore *self,
CallsUiCallData *call)
{
GObject * const call_obj = G_OBJECT (call);
GDateTime *start;
g_assert (g_object_get_data (call_obj, "calls-call-start") == NULL);
start = g_date_time_new_now_utc ();
g_object_set_data_full (call_obj, "calls-call-start",
start, (GDestroyNotify) g_date_time_unref);
if (!self->repository) {
open_repo (self);
return;
}
record_call (self, call);
g_signal_connect_swapped (call,
"state-changed",
G_CALLBACK (state_changed_cb),
self);
}
static void
call_removed_cb (CallsRecordStore *self,
CallsUiCallData *call,
const gchar *reason)
{
/* Stamp the call as ended if it hasn't already been done */
CallsCallRecord *record =
g_object_get_data (G_OBJECT (call), "calls-call-record");
if (record)
stamp_call (record, "end");
g_signal_handlers_disconnect_by_data (call, self);
}
static void
constructed (GObject *object)
{
g_autoptr (GList) calls = NULL;
GList *c;
CallsRecordStore *self = CALLS_RECORD_STORE (object);
open_repo (self);
g_signal_connect_swapped (calls_manager_get_default (),
"ui-call-added",
G_CALLBACK (call_added_cb),
self);
g_signal_connect_swapped (calls_manager_get_default (),
"ui-call-removed",
G_CALLBACK (call_removed_cb),
self);
calls = calls_manager_get_calls (calls_manager_get_default ());
for (c = calls; c != NULL; c = c->next) {
call_added_cb (self, c->data);
}
G_OBJECT_CLASS (calls_record_store_parent_class)->constructed (object);
}
static void
dispose (GObject *object)
{
CallsRecordStore *self = CALLS_RECORD_STORE (object);
g_list_store_remove_all (G_LIST_STORE (self));
g_clear_object (&self->repository);
G_OBJECT_CLASS (calls_record_store_parent_class)->dispose (object);
}
static void
finalize (GObject *object)
{
CallsRecordStore *self = CALLS_RECORD_STORE (object);
g_free (self->filename);
close_adapter (self);
G_OBJECT_CLASS (calls_record_store_parent_class)->finalize (object);
}
static void
calls_record_store_class_init (CallsRecordStoreClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
object_class->constructed = constructed;
object_class->dispose = dispose;
object_class->finalize = finalize;
signals[SIGNAL_DB_DONE] = g_signal_new ("db-done",
G_TYPE_FROM_CLASS (klass),
G_SIGNAL_RUN_LAST, 0, NULL, NULL,
NULL,
G_TYPE_NONE,
1,
G_TYPE_BOOLEAN);
}
static void
calls_record_store_init (CallsRecordStore *self)
{
g_autofree char *old_dir = g_build_filename (g_get_user_data_dir (),
"calls",
NULL);
g_autofree char *new_dir = g_build_filename (g_get_user_data_dir (),
APP_DATA_NAME,
NULL);
const char *env_dir = g_getenv ("CALLS_RECORD_DIR");
char *used_dir = NULL;
gboolean exist_old;
gboolean exist_new;
gboolean new_is_dir;
exist_old = g_file_test (old_dir, G_FILE_TEST_EXISTS);
exist_new = g_file_test (new_dir, G_FILE_TEST_EXISTS);
new_is_dir = g_file_test (new_dir, G_FILE_TEST_IS_DIR);
if (env_dir) {
used_dir = (char *) env_dir;
} else if (exist_old && !exist_new) {
g_debug ("Trying to move database from `%s' to `%s'", old_dir, new_dir);
if (g_rename (old_dir, new_dir) == 0) {
used_dir = new_dir;
} else {
g_warning ("Moving folders to new location failed!");
g_debug ("Continuing to use old location");
used_dir = old_dir;
}
} else if (exist_new && new_is_dir) {
used_dir = new_dir;
} else {
used_dir = old_dir;
}
g_assert (used_dir);
self->filename = g_build_filename (used_dir,
RECORD_STORE_FILENAME,
NULL);
}
CallsRecordStore *
calls_record_store_new (void)
{
return g_object_new (CALLS_TYPE_RECORD_STORE,
"item-type", CALLS_TYPE_CALL_RECORD,
NULL);
}