blob: 8499ac571a49f8b1ee6d68695df7170c94611e69 [file] [log] [blame]
Stepan Salenikovich76c33e62015-05-22 12:24:07 -04001/*
Stepan Salenikovichbe87d2c2016-01-25 14:14:34 -05002 * Copyright (C) 2015-2016 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 Salenikovich76c33e62015-05-22 12:24:07 -040036#endif
37
38void
39ring_notify_init()
40{
41#if USE_LIBNOTIFY
42 notify_init("Ring");
43#endif
44}
45
46void
47ring_notify_uninit()
48{
49#if USE_LIBNOTIFY
50 if (notify_is_initted())
51 notify_uninit();
52#endif
53}
54
55gboolean
56ring_notify_is_initted()
57{
58#if USE_LIBNOTIFY
59 return notify_is_initted();
60#else
61 return FALSE;
62#endif
63}
64
65gboolean
66ring_notify_incoming_call(
67#if !USE_LIBNOTIFY
68 G_GNUC_UNUSED
69#endif
70 Call* call)
71{
Stepan Salenikovich9675db32015-06-16 14:36:54 -040072 gboolean success = FALSE;
Stepan Salenikovich76c33e62015-05-22 12:24:07 -040073#if USE_LIBNOTIFY
74 g_return_val_if_fail(call, FALSE);
75
76 gchar *body = g_strdup_printf("%s", call->formattedName().toUtf8().constData());
Stepan Salenikovich9675db32015-06-16 14:36:54 -040077 std::shared_ptr<NotifyNotification> notification(
Stepan Salenikovicha1b8cb32015-09-11 14:58:35 -040078 notify_notification_new(_("Incoming call"), body, NULL), g_object_unref);
Stepan Salenikovichc2e44262015-06-16 12:46:33 -040079 g_free(body);
Stepan Salenikovich76c33e62015-05-22 12:24:07 -040080
81 /* get photo */
Stepan Salenikovichbbd6c132015-08-20 15:21:48 -040082 QVariant var_p = GlobalInstances::pixmapManipulator().callPhoto(
Stepan Salenikovich76c33e62015-05-22 12:24:07 -040083 call->peerContactMethod(), QSize(50, 50), false);
84 std::shared_ptr<GdkPixbuf> photo = var_p.value<std::shared_ptr<GdkPixbuf>>();
Stepan Salenikovich9675db32015-06-16 14:36:54 -040085 notify_notification_set_image_from_pixbuf(notification.get(), photo.get());
Stepan Salenikovich76c33e62015-05-22 12:24:07 -040086
87 /* calls have highest urgency */
Stepan Salenikovich9675db32015-06-16 14:36:54 -040088 notify_notification_set_urgency(notification.get(), NOTIFY_URGENCY_CRITICAL);
89 notify_notification_set_timeout(notification.get(), NOTIFY_EXPIRES_DEFAULT);
Stepan Salenikovich76c33e62015-05-22 12:24:07 -040090
91 GError *error = NULL;
Stepan Salenikovich9675db32015-06-16 14:36:54 -040092 success = notify_notification_show(notification.get(), &error);
93
94 if (success) {
95 /* monitor the life cycle of the call and try to close the notification
96 * once the call has been aswered */
97 auto state_changed_conn = std::make_shared<QMetaObject::Connection>();
98 *state_changed_conn = QObject::connect(
99 call,
100 &Call::lifeCycleStateChanged,
101 [notification, state_changed_conn] (Call::LifeCycleState newState, G_GNUC_UNUSED Call::LifeCycleState previousState)
102 {
103 g_return_if_fail(NOTIFY_IS_NOTIFICATION(notification.get()));
104 if (newState > Call::LifeCycleState::INITIALIZATION) {
105 /* note: not all systems will actually close the notification
106 * even if the above function returns as true */
107 if (!notify_notification_close(notification.get(), NULL))
108 g_warning("could not close notification");
109
110 /* once we (try to) close the notification, we can
111 * disconnect from this signal; this should also destroy
112 * the notification shared_ptr as its ref count will
113 * drop to 0 */
114 QObject::disconnect(*state_changed_conn);
115 }
116 }
117 );
Stepan Salenikovich76c33e62015-05-22 12:24:07 -0400118 } else {
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400119 g_warning("failed to show notification: %s", error->message);
Stepan Salenikovich76c33e62015-05-22 12:24:07 -0400120 g_clear_error(&error);
Stepan Salenikovich76c33e62015-05-22 12:24:07 -0400121 }
Stepan Salenikovich76c33e62015-05-22 12:24:07 -0400122#endif
Stepan Salenikovich9675db32015-06-16 14:36:54 -0400123 return success;
Stepan Salenikovich76c33e62015-05-22 12:24:07 -0400124}
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400125
126#if USE_LIBNOTIFY
127
128GHashTable *
129ring_notify_get_chat_table()
130{
131 static std::unique_ptr<GHashTable, decltype(g_hash_table_destroy)&> chat_table(
132 nullptr, g_hash_table_destroy);
133
134 if (chat_table.get() == nullptr)
135 chat_table.reset(g_hash_table_new_full(NULL, NULL, NULL, g_object_unref));
136
137 return chat_table.get();
138}
139
140static void
Stepan Salenikovich26cd1602016-01-20 13:43:17 -0500141notification_closed(NotifyNotification *notification, ContactMethod *cm)
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400142{
Stepan Salenikovich26cd1602016-01-20 13:43:17 -0500143 g_return_if_fail(cm);
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400144
Stepan Salenikovich26cd1602016-01-20 13:43:17 -0500145 if (!g_hash_table_remove(ring_notify_get_chat_table(), cm)) {
146 g_warning("could not find notification associated with the given ContactMethod");
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400147 /* normally removing the notification from the hash table will unref it,
148 * but if it was not found we should do it here */
149 g_object_unref(notification);
150 }
151}
152
153static gboolean
Stepan Salenikovich26cd1602016-01-20 13:43:17 -0500154ring_notify_show_text_message(ContactMethod *cm, const QModelIndex& idx)
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400155{
Stepan Salenikovich26cd1602016-01-20 13:43:17 -0500156 g_return_val_if_fail(idx.isValid() && cm, FALSE);
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400157 gboolean success = FALSE;
158
159 GHashTable *chat_table = ring_notify_get_chat_table();
160
Stepan Salenikovich26cd1602016-01-20 13:43:17 -0500161 auto title = g_strdup_printf(C_("Text message notification", "%s says:"), idx.data(static_cast<int>(Ring::Role::Name)).toString().toUtf8().constData());
162 auto body = g_strdup_printf("%s", idx.data(Qt::DisplayRole).toString().toUtf8().constData());
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400163
Stepan Salenikovich26cd1602016-01-20 13:43:17 -0500164 /* check if a notification already exists for this CM */
165 NotifyNotification *notification = (NotifyNotification *)g_hash_table_lookup(chat_table, cm);
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400166 if (notification) {
167 /* update notification; append the new message to the old */
168 GValue body_value = G_VALUE_INIT;
169 g_value_init(&body_value, G_TYPE_STRING);
170 g_object_get_property(G_OBJECT(notification), "body", &body_value);
171 const gchar* body_old = g_value_get_string(&body_value);
172 if (body_old && (strlen(body_old) > 0)) {
173 gchar *body_new = g_strconcat(body_old, "\n", body, NULL);
174 g_free(body);
175 body = body_new;
176 }
177 notify_notification_update(notification, title, body, NULL);
178 } else {
Stepan Salenikovich26cd1602016-01-20 13:43:17 -0500179 /* create new notification object and associate it with the CM in the
180 * hash table; also store the pointer of the CM in the notification
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400181 * object so that it knows it's key in the hash table */
182 notification = notify_notification_new(title, body, NULL);
Stepan Salenikovich26cd1602016-01-20 13:43:17 -0500183 g_hash_table_insert(chat_table, cm, notification);
184 g_object_set_data(G_OBJECT(notification), "ContactMethod", cm);
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400185
186 /* get photo */
Stepan Salenikovichbbd6c132015-08-20 15:21:48 -0400187 QVariant var_p = GlobalInstances::pixmapManipulator().callPhoto(
Stepan Salenikovich26cd1602016-01-20 13:43:17 -0500188 cm, QSize(50, 50), false);
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400189 std::shared_ptr<GdkPixbuf> photo = var_p.value<std::shared_ptr<GdkPixbuf>>();
190 notify_notification_set_image_from_pixbuf(notification, photo.get());
191
192 /* normal priority for messages */
193 notify_notification_set_urgency(notification, NOTIFY_URGENCY_NORMAL);
194
195 /* remove the key and value from the hash table once the notification is
196 * closed; note that this will also unref the notification */
Stepan Salenikovich26cd1602016-01-20 13:43:17 -0500197 g_signal_connect(notification, "closed", G_CALLBACK(notification_closed), cm);
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400198 }
199
200 g_free(title);
201 g_free(body);
202
203 GError *error = NULL;
204 success = notify_notification_show(notification, &error);
205 if (!success) {
206 g_warning("failed to show notification: %s", error->message);
207 g_clear_error(&error);
Stepan Salenikovich26cd1602016-01-20 13:43:17 -0500208 g_hash_table_remove(chat_table, cm);
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400209 }
210
211 return success;
212}
213
Stepan Salenikovich26cd1602016-01-20 13:43:17 -0500214static gboolean
215show_message_if_unread(const QModelIndex *idx)
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400216{
Stepan Salenikovich26cd1602016-01-20 13:43:17 -0500217 g_return_val_if_fail(idx && idx->isValid(), G_SOURCE_REMOVE);
218
219 if (!idx->data(static_cast<int>(Media::TextRecording::Role::IsRead)).toBool()) {
220 auto cm = idx->data(static_cast<int>(Media::TextRecording::Role::ContactMethod)).value<ContactMethod *>();
221 ring_notify_show_text_message(cm, *idx);
222 }
223
224 return G_SOURCE_REMOVE;
225}
226
227static void
228delete_idx(QModelIndex *idx)
229{
230 delete idx;
231}
232
233static void
234ring_notify_message(ContactMethod *cm, Media::TextRecording *t, RingClient *client)
235{
236 g_return_if_fail(cm && t && client);
237
238 // get the message
239 auto model = t->instantMessagingModel();
240 auto msg_idx = model->index(model->rowCount()-1, 0);
241
242 // make sure its a text message, or else there is nothing to do
243 if (msg_idx.data(static_cast<int>(Media::TextRecording::Role::HasText)).toBool()) {
244 auto main_window = ring_client_get_main_window(client);
245 if ( main_window && gtk_window_is_active(main_window)) {
246 /* in this case we only want to show the notification if the message is not marked as
247 * read; this will only possibly be done after the the chatview has displayed it in
248 * response to this or another signal; so we must check for the read status after the
249 * chat view handler has completed, we do so via a g_idle function.
250 */
251 auto new_idx = new QModelIndex(msg_idx);
252 g_idle_add_full(G_PRIORITY_DEFAULT_IDLE, (GSourceFunc)show_message_if_unread, new_idx, (GDestroyNotify)delete_idx);
253 } else {
254 /* always show a notification if the window is not active/visible */
255 ring_notify_show_text_message(cm, msg_idx);
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400256 }
Stepan Salenikovich26cd1602016-01-20 13:43:17 -0500257
258 }
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400259}
260#endif
261
262void
263ring_notify_monitor_chat_notifications(
264#if !USE_LIBNOTIFY
265 G_GNUC_UNUSED
266#endif
267 RingClient *client)
268{
269#if USE_LIBNOTIFY
270
271 QObject::connect(
Stepan Salenikovich26cd1602016-01-20 13:43:17 -0500272 &Media::RecordingModel::instance(),
273 &Media::RecordingModel::newTextMessage,
274 [client] (Media::TextRecording* t, ContactMethod* cm)
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400275 {
Stepan Salenikovich26cd1602016-01-20 13:43:17 -0500276 ring_notify_message(cm, t, client);
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400277 }
278 );
279#endif
280 }
281
282gboolean
283ring_notify_close_chat_notification(
284#if !USE_LIBNOTIFY
285 G_GNUC_UNUSED
286#endif
Stepan Salenikovich26cd1602016-01-20 13:43:17 -0500287 ContactMethod *cm)
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400288{
289 gboolean notification_existed = FALSE;
290
291#if USE_LIBNOTIFY
Stepan Salenikovich26cd1602016-01-20 13:43:17 -0500292 /* checks if there exists a chat notification associated with the given ContactMethod
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400293 * and tries to close it; if it did exist, then the function returns TRUE */
Stepan Salenikovich26cd1602016-01-20 13:43:17 -0500294 g_return_val_if_fail(cm, FALSE);
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400295
296
297 GHashTable *chat_table = ring_notify_get_chat_table();
298
Stepan Salenikovich26cd1602016-01-20 13:43:17 -0500299 NotifyNotification *notification = (NotifyNotification *)g_hash_table_lookup(chat_table, cm);
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400300
301 if (notification) {
302 notification_existed = TRUE;
303
304 GError *error = NULL;
305 if (!notify_notification_close(notification, &error)) {
306 g_warning("could not close notification: %s", error->message);
307 g_clear_error(&error);
308
309 /* closing should remove and free the notification from the hash table
310 * since it failed to close, try to remove the notification from the
311 * table manually */
Stepan Salenikovich26cd1602016-01-20 13:43:17 -0500312 g_hash_table_remove(chat_table, cm);
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400313 }
314 }
315#endif
316
317 return notification_existed;
318}