blob: 8a3b8b06fbf90ff05ec5db3399f7fbd57bbcbe2c [file] [log] [blame]
Stepan Salenikovich76c33e62015-05-22 12:24:07 -04001/*
Guillaume Roguez2a6150d2017-07-19 18:24:47 -04002 * Copyright (C) 2015-2017 Savoir-faire Linux Inc.
Stepan Salenikovich76c33e62015-05-22 12:24:07 -04003 * Author: Stepan Salenikovich <stepan.salenikovich@savoirfairelinux.com>
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program; if not, write to the Free Software
17 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Stepan Salenikovich76c33e62015-05-22 12:24:07 -040018 */
19
20#include "ringnotify.h"
21#include "config.h"
Stepan Salenikovich5834f832016-05-20 15:32:29 -040022#include "ring_client.h"
Stepan Salenikovich76c33e62015-05-22 12:24:07 -040023
24#if USE_LIBNOTIFY
Stepan Salenikovicha1b8cb32015-09-11 14:58:35 -040025#include <glib/gi18n.h>
Stepan Salenikovich76c33e62015-05-22 12:24:07 -040026#include <libnotify/notify.h>
27#include <memory>
Stepan Salenikovichbbd6c132015-08-20 15:21:48 -040028#include <globalinstances.h>
29#include "native/pixbufmanipulator.h"
Stepan Salenikovich76c33e62015-05-22 12:24:07 -040030#include <call.h>
31#include <QtCore/QSize>
Stepan Salenikovich67112d12015-06-16 16:57:06 -040032#include <media/text.h>
33#include <callmodel.h>
Stepan Salenikovich26cd1602016-01-20 13:43:17 -050034#include <media/textrecording.h>
35#include <media/recordingmodel.h>
Stepan Salenikovichf8e78cb2016-09-12 17:14:51 -040036#include <recentmodel.h>
Stepan Salenikovich76c33e62015-05-22 12:24:07 -040037#endif
38
Stepan Salenikovichfc79fc12016-09-12 15:29:38 -040039#if USE_LIBNOTIFY
40
41static constexpr int MAX_NOTIFICATIONS = 10; // max unread chat msgs to display from the same contact
42static constexpr const char* SERVER_NOTIFY_OSD = "notify-osd";
43
44/* struct to store the parsed list of the notify server capabilities */
45struct RingNotifyServerInfo
46{
47 /* info */
48 char *name;
49 char *vendor;
50 char *version;
51 char *spec;
52
53 /* capabilities */
54 gboolean append;
55 gboolean actions;
56
57 /* the info strings must be freed */
58 ~RingNotifyServerInfo() {
59 g_free(name);
60 g_free(vendor);
61 g_free(version);
62 g_free(spec);
63 }
64};
65
66static struct RingNotifyServerInfo server_info;
67#endif
68
Stepan Salenikovich76c33e62015-05-22 12:24:07 -040069void
70ring_notify_init()
71{
72#if USE_LIBNOTIFY
73 notify_init("Ring");
Stepan Salenikovichfc79fc12016-09-12 15:29:38 -040074
75 /* get notify server info */
76 if (notify_get_server_info(&server_info.name,
77 &server_info.vendor,
78 &server_info.version,
79 &server_info.spec)) {
80 g_debug("notify server name: %s, vendor: %s, version: %s, spec: %s",
81 server_info.name, server_info.vendor, server_info.version, server_info.spec);
82 }
83
84 /* check notify server capabilities */
85 auto list = notify_get_server_caps();
86 while (list) {
87 if (g_strcmp0((const char *)list->data, "append") == 0 ||
88 g_strcmp0((const char *)list->data, "x-canonical-append") == 0) {
89 server_info.append = TRUE;
90 }
91 if (g_strcmp0((const char *)list->data, "actions") == 0) {
92 server_info.actions = TRUE;
93 }
94
95 list = g_list_next(list);
96 }
97
98 g_list_free_full(list, g_free);
Stepan Salenikovich76c33e62015-05-22 12:24:07 -040099#endif
100}
101
102void
103ring_notify_uninit()
104{
105#if USE_LIBNOTIFY
106 if (notify_is_initted())
107 notify_uninit();
108#endif
109}
110
111gboolean
112ring_notify_is_initted()
113{
114#if USE_LIBNOTIFY
115 return notify_is_initted();
116#else
117 return FALSE;
118#endif
119}
120
Stepan Salenikovichf8e78cb2016-09-12 17:14:51 -0400121static void
122ring_notify_show_cm(NotifyNotification*, char *, ContactMethod *cm)
123{
124 /* show the main window in case its hidden */
125 if (auto action = g_action_map_lookup_action(G_ACTION_MAP(g_application_get_default()), "show-main-window")) {
126 g_action_change_state(action, g_variant_new_boolean(TRUE));
127 }
128 /* select the relevant cm */
129 auto idx = RecentModel::instance().getIndex(cm);
130 if (idx.isValid()) {
131 RecentModel::instance().selectionModel()->setCurrentIndex(idx, QItemSelectionModel::ClearAndSelect);
132 }
133}
134
Stepan Salenikovich76c33e62015-05-22 12:24:07 -0400135gboolean
136ring_notify_incoming_call(
137#if !USE_LIBNOTIFY
138 G_GNUC_UNUSED
139#endif
140 Call* call)
141{
Stepan Salenikovich9675db32015-06-16 14:36:54 -0400142 gboolean success = FALSE;
Stepan Salenikovich76c33e62015-05-22 12:24:07 -0400143#if USE_LIBNOTIFY
144 g_return_val_if_fail(call, FALSE);
145
Stepan Salenikovichf8e78cb2016-09-12 17:14:51 -0400146 gchar *body = g_markup_escape_text(call->formattedName().toUtf8().constData(), -1);
Stepan Salenikovich9675db32015-06-16 14:36:54 -0400147 std::shared_ptr<NotifyNotification> notification(
Stepan Salenikovicha1b8cb32015-09-11 14:58:35 -0400148 notify_notification_new(_("Incoming call"), body, NULL), g_object_unref);
Stepan Salenikovichc2e44262015-06-16 12:46:33 -0400149 g_free(body);
Stepan Salenikovich76c33e62015-05-22 12:24:07 -0400150
151 /* get photo */
Stepan Salenikovichbbd6c132015-08-20 15:21:48 -0400152 QVariant var_p = GlobalInstances::pixmapManipulator().callPhoto(
Stepan Salenikovich76c33e62015-05-22 12:24:07 -0400153 call->peerContactMethod(), QSize(50, 50), false);
154 std::shared_ptr<GdkPixbuf> photo = var_p.value<std::shared_ptr<GdkPixbuf>>();
Stepan Salenikovich9675db32015-06-16 14:36:54 -0400155 notify_notification_set_image_from_pixbuf(notification.get(), photo.get());
Stepan Salenikovich76c33e62015-05-22 12:24:07 -0400156
157 /* calls have highest urgency */
Stepan Salenikovich9675db32015-06-16 14:36:54 -0400158 notify_notification_set_urgency(notification.get(), NOTIFY_URGENCY_CRITICAL);
159 notify_notification_set_timeout(notification.get(), NOTIFY_EXPIRES_DEFAULT);
Stepan Salenikovich76c33e62015-05-22 12:24:07 -0400160
Stepan Salenikovichf8e78cb2016-09-12 17:14:51 -0400161 /* if the notification server supports actions, make the default action to show the call */
162 if (server_info.actions) {
163 notify_notification_add_action(notification.get(),
164 "default",
165 C_("notification action name", "Show"),
166 (NotifyActionCallback)ring_notify_show_cm,
167 call->peerContactMethod(),
168 nullptr);
169 }
170
Stepan Salenikovich76c33e62015-05-22 12:24:07 -0400171 GError *error = NULL;
Stepan Salenikovich9675db32015-06-16 14:36:54 -0400172 success = notify_notification_show(notification.get(), &error);
173
174 if (success) {
175 /* monitor the life cycle of the call and try to close the notification
176 * once the call has been aswered */
177 auto state_changed_conn = std::make_shared<QMetaObject::Connection>();
178 *state_changed_conn = QObject::connect(
179 call,
180 &Call::lifeCycleStateChanged,
181 [notification, state_changed_conn] (Call::LifeCycleState newState, G_GNUC_UNUSED Call::LifeCycleState previousState)
182 {
183 g_return_if_fail(NOTIFY_IS_NOTIFICATION(notification.get()));
184 if (newState > Call::LifeCycleState::INITIALIZATION) {
185 /* note: not all systems will actually close the notification
186 * even if the above function returns as true */
187 if (!notify_notification_close(notification.get(), NULL))
188 g_warning("could not close notification");
189
190 /* once we (try to) close the notification, we can
191 * disconnect from this signal; this should also destroy
192 * the notification shared_ptr as its ref count will
193 * drop to 0 */
194 QObject::disconnect(*state_changed_conn);
195 }
196 }
197 );
Stepan Salenikovich76c33e62015-05-22 12:24:07 -0400198 } else {
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400199 g_warning("failed to show notification: %s", error->message);
Stepan Salenikovich76c33e62015-05-22 12:24:07 -0400200 g_clear_error(&error);
Stepan Salenikovich76c33e62015-05-22 12:24:07 -0400201 }
Stepan Salenikovich76c33e62015-05-22 12:24:07 -0400202#endif
Stepan Salenikovich9675db32015-06-16 14:36:54 -0400203 return success;
Stepan Salenikovich76c33e62015-05-22 12:24:07 -0400204}
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400205
206#if USE_LIBNOTIFY
207
Stepan Salenikovichfc79fc12016-09-12 15:29:38 -0400208static void
209ring_notify_free_list(gpointer, GList *value, gpointer)
210{
211 if (value) {
212 g_object_unref(G_OBJECT(value->data));
213 g_list_free(value);
214 }
215}
216
217static void
218ring_notify_free_chat_table(GHashTable *table) {
219 if (table) {
220 g_hash_table_foreach(table, (GHFunc)ring_notify_free_list, nullptr);
221 g_hash_table_destroy(table);
222 }
223}
224
225/**
226 * Returns a pointer to a GHashTable which contains key,value pairs where a ContactMethod pointer
227 * is the key and a GList of notifications for that CM is the vlue.
228 */
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400229GHashTable *
230ring_notify_get_chat_table()
231{
Stepan Salenikovichfc79fc12016-09-12 15:29:38 -0400232 static std::unique_ptr<GHashTable, decltype(ring_notify_free_chat_table)&> chat_table(
233 nullptr, ring_notify_free_chat_table);
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400234
235 if (chat_table.get() == nullptr)
Stepan Salenikovichfc79fc12016-09-12 15:29:38 -0400236 chat_table.reset(g_hash_table_new(NULL, NULL));
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400237
238 return chat_table.get();
239}
240
241static void
Stepan Salenikovich26cd1602016-01-20 13:43:17 -0500242notification_closed(NotifyNotification *notification, ContactMethod *cm)
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400243{
Stepan Salenikovich26cd1602016-01-20 13:43:17 -0500244 g_return_if_fail(cm);
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400245
Stepan Salenikovichfc79fc12016-09-12 15:29:38 -0400246 /* remove from the list */
247 auto chat_table = ring_notify_get_chat_table();
248 if (auto list = (GList *)g_hash_table_lookup(chat_table, cm)) {
249 list = g_list_remove(list, notification);
250 if (list) {
251 // the head of the list may have changed
252 g_hash_table_replace(chat_table, cm, list);
253 } else {
254 g_hash_table_remove(chat_table, cm);
255 }
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400256 }
Stepan Salenikovichfc79fc12016-09-12 15:29:38 -0400257
258 g_object_unref(notification);
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400259}
260
261static gboolean
Stepan Salenikovich26cd1602016-01-20 13:43:17 -0500262ring_notify_show_text_message(ContactMethod *cm, const QModelIndex& idx)
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400263{
Stepan Salenikovich26cd1602016-01-20 13:43:17 -0500264 g_return_val_if_fail(idx.isValid() && cm, FALSE);
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400265 gboolean success = FALSE;
266
Stepan Salenikovichfc79fc12016-09-12 15:29:38 -0400267 auto title = g_markup_printf_escaped(C_("Text message notification", "%s says:"), idx.data(static_cast<int>(Ring::Role::Name)).toString().toUtf8().constData());
268 auto body = g_markup_escape_text(idx.data(Qt::DisplayRole).toString().toUtf8().constData(), -1);
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400269
Stepan Salenikovichfc79fc12016-09-12 15:29:38 -0400270 NotifyNotification *notification_new = nullptr;
271 NotifyNotification *notification_old = nullptr;
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400272
Stepan Salenikovichfc79fc12016-09-12 15:29:38 -0400273 /* try to get the previous notification */
274 auto chat_table = ring_notify_get_chat_table();
275 auto list = (GList *)g_hash_table_lookup(chat_table, cm);
276 if (list)
277 notification_old = (NotifyNotification *)list->data;
278
279 /* we display chat notifications in different ways to suit different notification servers and
280 * their capabilities:
281 * 1. if the server doesn't support appending (eg: Notification Daemon) then we update the
282 * previous notification (if exists) with new text; otherwise it takes we have many
283 * notifications from the same person... we don't concatinate the old messages because
284 * servers which don't support append usually don't support multi line bodies
285 * 2. the notify-osd server supports appending; however it doesn't clear the old notifications
286 * on demand, which means in our case that chat messages which have already been read could
287 * still be displayed when a new notification is appended, thus in this case, we update
288 * the old notification body manually to only contain the unread messages
289 * 3. the 3rd case is that the server supports append but is not notify-osd, then we simply use
290 * the append feature
291 */
292
293 if (notification_old && !server_info.append) {
294 /* case 1 */
295 notify_notification_update(notification_old, title, body, nullptr);
296 notification_new = notification_old;
297 } else if (notification_old && g_strcmp0(server_info.name, SERVER_NOTIFY_OSD) == 0) {
298 /* case 2 */
299 /* print up to MAX_NOTIFICATIONS unread messages */
300 int msg_count = 0;
301 auto idx_next = idx.sibling(idx.row() - 1, idx.column());
302 auto read = idx_next.data(static_cast<int>(Media::TextRecording::Role::IsRead)).toBool();
303 while (idx_next.isValid() && !read && msg_count < MAX_NOTIFICATIONS) {
304
305 auto body_prev = body;
306 body = g_markup_printf_escaped("%s\n%s", body_prev, idx_next.data(Qt::DisplayRole).toString().toUtf8().constData());
307 g_free(body_prev);
308
309 idx_next = idx_next.sibling(idx_next.row() - 1, idx_next.column());
310 read = idx_next.data(static_cast<int>(Media::TextRecording::Role::IsRead)).toBool();
311 ++msg_count;
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400312 }
Stepan Salenikovichfc79fc12016-09-12 15:29:38 -0400313
314 notify_notification_update(notification_old, title, body, nullptr);
315
316 notification_new = notification_old;
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400317 } else {
Stepan Salenikovichfc79fc12016-09-12 15:29:38 -0400318 /* need new notification for case 1, 2, or 3 */
319 notification_new = notify_notification_new(title, body, nullptr);
320
321 /* track in hash table */
322 auto list = (GList *)g_hash_table_lookup(chat_table, cm);
323 list = g_list_append(list, notification_new);
324 g_hash_table_replace(chat_table, cm, list);
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400325
326 /* get photo */
Stepan Salenikovichbbd6c132015-08-20 15:21:48 -0400327 QVariant var_p = GlobalInstances::pixmapManipulator().callPhoto(
Stepan Salenikovich26cd1602016-01-20 13:43:17 -0500328 cm, QSize(50, 50), false);
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400329 std::shared_ptr<GdkPixbuf> photo = var_p.value<std::shared_ptr<GdkPixbuf>>();
Stepan Salenikovichfc79fc12016-09-12 15:29:38 -0400330 notify_notification_set_image_from_pixbuf(notification_new, photo.get());
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400331
332 /* normal priority for messages */
Stepan Salenikovichfc79fc12016-09-12 15:29:38 -0400333 notify_notification_set_urgency(notification_new, NOTIFY_URGENCY_NORMAL);
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400334
335 /* remove the key and value from the hash table once the notification is
336 * closed; note that this will also unref the notification */
Stepan Salenikovichfc79fc12016-09-12 15:29:38 -0400337 g_signal_connect(notification_new, "closed", G_CALLBACK(notification_closed), cm);
Stepan Salenikovichf8e78cb2016-09-12 17:14:51 -0400338
339 /* if the notification server supports actions, make the default action to show the chat view */
340 if (server_info.actions) {
341 notify_notification_add_action(notification_new,
342 "default",
343 C_("notification action name", "Show"),
344 (NotifyActionCallback)ring_notify_show_cm,
345 cm,
346 nullptr);
347 }
Stepan Salenikovichfc79fc12016-09-12 15:29:38 -0400348 }
349
350 GError *error = nullptr;
351 success = notify_notification_show(notification_new, &error);
352 if (!success) {
353 g_warning("failed to show notification: %s", error->message);
354 g_clear_error(&error);
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400355 }
356
357 g_free(title);
358 g_free(body);
359
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400360 return success;
361}
362
Stepan Salenikovich26cd1602016-01-20 13:43:17 -0500363static gboolean
364show_message_if_unread(const QModelIndex *idx)
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400365{
Stepan Salenikovich26cd1602016-01-20 13:43:17 -0500366 g_return_val_if_fail(idx && idx->isValid(), G_SOURCE_REMOVE);
367
368 if (!idx->data(static_cast<int>(Media::TextRecording::Role::IsRead)).toBool()) {
369 auto cm = idx->data(static_cast<int>(Media::TextRecording::Role::ContactMethod)).value<ContactMethod *>();
370 ring_notify_show_text_message(cm, *idx);
371 }
372
373 return G_SOURCE_REMOVE;
374}
375
376static void
377delete_idx(QModelIndex *idx)
378{
Nicolas Jagerfad674f2017-10-19 11:35:36 -0400379 if (idx) {
380 delete idx;
381 idx = nullptr;
382 }
Stepan Salenikovich26cd1602016-01-20 13:43:17 -0500383}
384
Stepan Salenikovich5a127672016-09-13 11:19:50 -0400385#endif
386
387void
388ring_notify_message(
389#if !USE_LIBNOTIFY
390 ContactMethod*, Media::TextRecording*, RingClient*)
391#else
392 ContactMethod *cm, Media::TextRecording *t, RingClient *client)
393#endif
Stepan Salenikovich26cd1602016-01-20 13:43:17 -0500394{
Stepan Salenikovich5a127672016-09-13 11:19:50 -0400395
396#if USE_LIBNOTIFY
Stepan Salenikovich26cd1602016-01-20 13:43:17 -0500397 g_return_if_fail(cm && t && client);
398
399 // get the message
400 auto model = t->instantMessagingModel();
401 auto msg_idx = model->index(model->rowCount()-1, 0);
402
403 // make sure its a text message, or else there is nothing to do
404 if (msg_idx.data(static_cast<int>(Media::TextRecording::Role::HasText)).toBool()) {
405 auto main_window = ring_client_get_main_window(client);
406 if ( main_window && gtk_window_is_active(main_window)) {
407 /* in this case we only want to show the notification if the message is not marked as
408 * read; this will only possibly be done after the the chatview has displayed it in
409 * response to this or another signal; so we must check for the read status after the
410 * chat view handler has completed, we do so via a g_idle function.
411 */
412 auto new_idx = new QModelIndex(msg_idx);
413 g_idle_add_full(G_PRIORITY_DEFAULT_IDLE, (GSourceFunc)show_message_if_unread, new_idx, (GDestroyNotify)delete_idx);
414 } else {
415 /* always show a notification if the window is not active/visible */
416 ring_notify_show_text_message(cm, msg_idx);
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400417 }
Stepan Salenikovich26cd1602016-01-20 13:43:17 -0500418
419 }
Stepan Salenikovich5a127672016-09-13 11:19:50 -0400420#endif
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400421}
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400422
423gboolean
424ring_notify_close_chat_notification(
425#if !USE_LIBNOTIFY
426 G_GNUC_UNUSED
427#endif
Stepan Salenikovich26cd1602016-01-20 13:43:17 -0500428 ContactMethod *cm)
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400429{
430 gboolean notification_existed = FALSE;
431
432#if USE_LIBNOTIFY
Stepan Salenikovich26cd1602016-01-20 13:43:17 -0500433 /* checks if there exists a chat notification associated with the given ContactMethod
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400434 * and tries to close it; if it did exist, then the function returns TRUE */
Stepan Salenikovich26cd1602016-01-20 13:43:17 -0500435 g_return_val_if_fail(cm, FALSE);
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400436
437
Stepan Salenikovichfc79fc12016-09-12 15:29:38 -0400438 auto chat_table = ring_notify_get_chat_table();
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400439
Stepan Salenikovichfc79fc12016-09-12 15:29:38 -0400440 if (auto list = (GList *)g_hash_table_lookup(chat_table, cm)) {
441 while (list) {
442 notification_existed = TRUE;
443 auto notification = (NotifyNotification *)list->data;
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400444
Stepan Salenikovichfc79fc12016-09-12 15:29:38 -0400445 GError *error = NULL;
446 if (!notify_notification_close(notification, &error)) {
447 g_warning("could not close notification: %s", error->message);
448 g_clear_error(&error);
449 }
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400450
Stepan Salenikovichfc79fc12016-09-12 15:29:38 -0400451 list = g_list_next(list);
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400452 }
453 }
Stepan Salenikovichfc79fc12016-09-12 15:29:38 -0400454
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400455#endif
456
457 return notification_existed;
458}