blob: 8511ab5c8c6304f082f908c1610300a83074ffaa [file] [log] [blame]
/*
* Copyright (C) 2015-2016 Savoir-faire Linux Inc.
* Author: Stepan Salenikovich <stepan.salenikovich@savoirfairelinux.com>
*
* 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 3 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.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#include "ringnotify.h"
#include "config.h"
#include "ring_client.h"
#if USE_LIBNOTIFY
#include <glib/gi18n.h>
#include <libnotify/notify.h>
#include <memory>
#include <globalinstances.h>
#include "native/pixbufmanipulator.h"
#include <call.h>
#include <QtCore/QSize>
#include <media/text.h>
#include <callmodel.h>
#include <media/textrecording.h>
#include <media/recordingmodel.h>
#include <recentmodel.h>
#endif
#if USE_LIBNOTIFY
static constexpr int MAX_NOTIFICATIONS = 10; // max unread chat msgs to display from the same contact
static constexpr const char* SERVER_NOTIFY_OSD = "notify-osd";
/* struct to store the parsed list of the notify server capabilities */
struct RingNotifyServerInfo
{
/* info */
char *name;
char *vendor;
char *version;
char *spec;
/* capabilities */
gboolean append;
gboolean actions;
/* the info strings must be freed */
~RingNotifyServerInfo() {
g_free(name);
g_free(vendor);
g_free(version);
g_free(spec);
}
};
static struct RingNotifyServerInfo server_info;
#endif
void
ring_notify_init()
{
#if USE_LIBNOTIFY
notify_init("Ring");
/* get notify server info */
if (notify_get_server_info(&server_info.name,
&server_info.vendor,
&server_info.version,
&server_info.spec)) {
g_debug("notify server name: %s, vendor: %s, version: %s, spec: %s",
server_info.name, server_info.vendor, server_info.version, server_info.spec);
}
/* check notify server capabilities */
auto list = notify_get_server_caps();
while (list) {
if (g_strcmp0((const char *)list->data, "append") == 0 ||
g_strcmp0((const char *)list->data, "x-canonical-append") == 0) {
server_info.append = TRUE;
}
if (g_strcmp0((const char *)list->data, "actions") == 0) {
server_info.actions = TRUE;
}
list = g_list_next(list);
}
g_list_free_full(list, g_free);
#endif
}
void
ring_notify_uninit()
{
#if USE_LIBNOTIFY
if (notify_is_initted())
notify_uninit();
#endif
}
gboolean
ring_notify_is_initted()
{
#if USE_LIBNOTIFY
return notify_is_initted();
#else
return FALSE;
#endif
}
static void
ring_notify_show_cm(NotifyNotification*, char *, ContactMethod *cm)
{
/* show the main window in case its hidden */
if (auto action = g_action_map_lookup_action(G_ACTION_MAP(g_application_get_default()), "show-main-window")) {
g_action_change_state(action, g_variant_new_boolean(TRUE));
}
/* select the relevant cm */
auto idx = RecentModel::instance().getIndex(cm);
if (idx.isValid()) {
RecentModel::instance().selectionModel()->setCurrentIndex(idx, QItemSelectionModel::ClearAndSelect);
}
}
gboolean
ring_notify_incoming_call(
#if !USE_LIBNOTIFY
G_GNUC_UNUSED
#endif
Call* call)
{
gboolean success = FALSE;
#if USE_LIBNOTIFY
g_return_val_if_fail(call, FALSE);
gchar *body = g_markup_escape_text(call->formattedName().toUtf8().constData(), -1);
std::shared_ptr<NotifyNotification> notification(
notify_notification_new(_("Incoming call"), body, NULL), g_object_unref);
g_free(body);
/* get photo */
QVariant var_p = GlobalInstances::pixmapManipulator().callPhoto(
call->peerContactMethod(), QSize(50, 50), false);
std::shared_ptr<GdkPixbuf> photo = var_p.value<std::shared_ptr<GdkPixbuf>>();
notify_notification_set_image_from_pixbuf(notification.get(), photo.get());
/* calls have highest urgency */
notify_notification_set_urgency(notification.get(), NOTIFY_URGENCY_CRITICAL);
notify_notification_set_timeout(notification.get(), NOTIFY_EXPIRES_DEFAULT);
/* if the notification server supports actions, make the default action to show the call */
if (server_info.actions) {
notify_notification_add_action(notification.get(),
"default",
C_("notification action name", "Show"),
(NotifyActionCallback)ring_notify_show_cm,
call->peerContactMethod(),
nullptr);
}
GError *error = NULL;
success = notify_notification_show(notification.get(), &error);
if (success) {
/* monitor the life cycle of the call and try to close the notification
* once the call has been aswered */
auto state_changed_conn = std::make_shared<QMetaObject::Connection>();
*state_changed_conn = QObject::connect(
call,
&Call::lifeCycleStateChanged,
[notification, state_changed_conn] (Call::LifeCycleState newState, G_GNUC_UNUSED Call::LifeCycleState previousState)
{
g_return_if_fail(NOTIFY_IS_NOTIFICATION(notification.get()));
if (newState > Call::LifeCycleState::INITIALIZATION) {
/* note: not all systems will actually close the notification
* even if the above function returns as true */
if (!notify_notification_close(notification.get(), NULL))
g_warning("could not close notification");
/* once we (try to) close the notification, we can
* disconnect from this signal; this should also destroy
* the notification shared_ptr as its ref count will
* drop to 0 */
QObject::disconnect(*state_changed_conn);
}
}
);
} else {
g_warning("failed to show notification: %s", error->message);
g_clear_error(&error);
}
#endif
return success;
}
#if USE_LIBNOTIFY
static void
ring_notify_free_list(gpointer, GList *value, gpointer)
{
if (value) {
g_object_unref(G_OBJECT(value->data));
g_list_free(value);
}
}
static void
ring_notify_free_chat_table(GHashTable *table) {
if (table) {
g_hash_table_foreach(table, (GHFunc)ring_notify_free_list, nullptr);
g_hash_table_destroy(table);
}
}
/**
* Returns a pointer to a GHashTable which contains key,value pairs where a ContactMethod pointer
* is the key and a GList of notifications for that CM is the vlue.
*/
GHashTable *
ring_notify_get_chat_table()
{
static std::unique_ptr<GHashTable, decltype(ring_notify_free_chat_table)&> chat_table(
nullptr, ring_notify_free_chat_table);
if (chat_table.get() == nullptr)
chat_table.reset(g_hash_table_new(NULL, NULL));
return chat_table.get();
}
static void
notification_closed(NotifyNotification *notification, ContactMethod *cm)
{
g_return_if_fail(cm);
/* remove from the list */
auto chat_table = ring_notify_get_chat_table();
if (auto list = (GList *)g_hash_table_lookup(chat_table, cm)) {
list = g_list_remove(list, notification);
if (list) {
// the head of the list may have changed
g_hash_table_replace(chat_table, cm, list);
} else {
g_hash_table_remove(chat_table, cm);
}
}
g_object_unref(notification);
}
static gboolean
ring_notify_show_text_message(ContactMethod *cm, const QModelIndex& idx)
{
g_return_val_if_fail(idx.isValid() && cm, FALSE);
gboolean success = FALSE;
auto title = g_markup_printf_escaped(C_("Text message notification", "%s says:"), idx.data(static_cast<int>(Ring::Role::Name)).toString().toUtf8().constData());
auto body = g_markup_escape_text(idx.data(Qt::DisplayRole).toString().toUtf8().constData(), -1);
NotifyNotification *notification_new = nullptr;
NotifyNotification *notification_old = nullptr;
/* try to get the previous notification */
auto chat_table = ring_notify_get_chat_table();
auto list = (GList *)g_hash_table_lookup(chat_table, cm);
if (list)
notification_old = (NotifyNotification *)list->data;
/* we display chat notifications in different ways to suit different notification servers and
* their capabilities:
* 1. if the server doesn't support appending (eg: Notification Daemon) then we update the
* previous notification (if exists) with new text; otherwise it takes we have many
* notifications from the same person... we don't concatinate the old messages because
* servers which don't support append usually don't support multi line bodies
* 2. the notify-osd server supports appending; however it doesn't clear the old notifications
* on demand, which means in our case that chat messages which have already been read could
* still be displayed when a new notification is appended, thus in this case, we update
* the old notification body manually to only contain the unread messages
* 3. the 3rd case is that the server supports append but is not notify-osd, then we simply use
* the append feature
*/
if (notification_old && !server_info.append) {
/* case 1 */
notify_notification_update(notification_old, title, body, nullptr);
notification_new = notification_old;
} else if (notification_old && g_strcmp0(server_info.name, SERVER_NOTIFY_OSD) == 0) {
/* case 2 */
/* print up to MAX_NOTIFICATIONS unread messages */
int msg_count = 0;
auto idx_next = idx.sibling(idx.row() - 1, idx.column());
auto read = idx_next.data(static_cast<int>(Media::TextRecording::Role::IsRead)).toBool();
while (idx_next.isValid() && !read && msg_count < MAX_NOTIFICATIONS) {
auto body_prev = body;
body = g_markup_printf_escaped("%s\n%s", body_prev, idx_next.data(Qt::DisplayRole).toString().toUtf8().constData());
g_free(body_prev);
idx_next = idx_next.sibling(idx_next.row() - 1, idx_next.column());
read = idx_next.data(static_cast<int>(Media::TextRecording::Role::IsRead)).toBool();
++msg_count;
}
notify_notification_update(notification_old, title, body, nullptr);
notification_new = notification_old;
} else {
/* need new notification for case 1, 2, or 3 */
notification_new = notify_notification_new(title, body, nullptr);
/* track in hash table */
auto list = (GList *)g_hash_table_lookup(chat_table, cm);
list = g_list_append(list, notification_new);
g_hash_table_replace(chat_table, cm, list);
/* get photo */
QVariant var_p = GlobalInstances::pixmapManipulator().callPhoto(
cm, QSize(50, 50), false);
std::shared_ptr<GdkPixbuf> photo = var_p.value<std::shared_ptr<GdkPixbuf>>();
notify_notification_set_image_from_pixbuf(notification_new, photo.get());
/* normal priority for messages */
notify_notification_set_urgency(notification_new, NOTIFY_URGENCY_NORMAL);
/* remove the key and value from the hash table once the notification is
* closed; note that this will also unref the notification */
g_signal_connect(notification_new, "closed", G_CALLBACK(notification_closed), cm);
/* if the notification server supports actions, make the default action to show the chat view */
if (server_info.actions) {
notify_notification_add_action(notification_new,
"default",
C_("notification action name", "Show"),
(NotifyActionCallback)ring_notify_show_cm,
cm,
nullptr);
}
}
GError *error = nullptr;
success = notify_notification_show(notification_new, &error);
if (!success) {
g_warning("failed to show notification: %s", error->message);
g_clear_error(&error);
}
g_free(title);
g_free(body);
return success;
}
static gboolean
show_message_if_unread(const QModelIndex *idx)
{
g_return_val_if_fail(idx && idx->isValid(), G_SOURCE_REMOVE);
if (!idx->data(static_cast<int>(Media::TextRecording::Role::IsRead)).toBool()) {
auto cm = idx->data(static_cast<int>(Media::TextRecording::Role::ContactMethod)).value<ContactMethod *>();
ring_notify_show_text_message(cm, *idx);
}
return G_SOURCE_REMOVE;
}
static void
delete_idx(QModelIndex *idx)
{
delete idx;
}
#endif
void
ring_notify_message(
#if !USE_LIBNOTIFY
ContactMethod*, Media::TextRecording*, RingClient*)
#else
ContactMethod *cm, Media::TextRecording *t, RingClient *client)
#endif
{
#if USE_LIBNOTIFY
g_return_if_fail(cm && t && client);
// get the message
auto model = t->instantMessagingModel();
auto msg_idx = model->index(model->rowCount()-1, 0);
// make sure its a text message, or else there is nothing to do
if (msg_idx.data(static_cast<int>(Media::TextRecording::Role::HasText)).toBool()) {
auto main_window = ring_client_get_main_window(client);
if ( main_window && gtk_window_is_active(main_window)) {
/* in this case we only want to show the notification if the message is not marked as
* read; this will only possibly be done after the the chatview has displayed it in
* response to this or another signal; so we must check for the read status after the
* chat view handler has completed, we do so via a g_idle function.
*/
auto new_idx = new QModelIndex(msg_idx);
g_idle_add_full(G_PRIORITY_DEFAULT_IDLE, (GSourceFunc)show_message_if_unread, new_idx, (GDestroyNotify)delete_idx);
} else {
/* always show a notification if the window is not active/visible */
ring_notify_show_text_message(cm, msg_idx);
}
}
#endif
}
gboolean
ring_notify_close_chat_notification(
#if !USE_LIBNOTIFY
G_GNUC_UNUSED
#endif
ContactMethod *cm)
{
gboolean notification_existed = FALSE;
#if USE_LIBNOTIFY
/* checks if there exists a chat notification associated with the given ContactMethod
* and tries to close it; if it did exist, then the function returns TRUE */
g_return_val_if_fail(cm, FALSE);
auto chat_table = ring_notify_get_chat_table();
if (auto list = (GList *)g_hash_table_lookup(chat_table, cm)) {
while (list) {
notification_existed = TRUE;
auto notification = (NotifyNotification *)list->data;
GError *error = NULL;
if (!notify_notification_close(notification, &error)) {
g_warning("could not close notification: %s", error->message);
g_clear_error(&error);
}
list = g_list_next(list);
}
}
#endif
return notification_existed;
}