blob: 0d5b1ae7c7063b257ac6cd19a7e3a1b485f85c2c [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"
22
23#if USE_LIBNOTIFY
Stepan Salenikovicha1b8cb32015-09-11 14:58:35 -040024#include <glib/gi18n.h>
Stepan Salenikovich76c33e62015-05-22 12:24:07 -040025#include <libnotify/notify.h>
26#include <memory>
Stepan Salenikovichbbd6c132015-08-20 15:21:48 -040027#include <globalinstances.h>
28#include "native/pixbufmanipulator.h"
Stepan Salenikovich76c33e62015-05-22 12:24:07 -040029#include <call.h>
30#include <QtCore/QSize>
Stepan Salenikovich67112d12015-06-16 16:57:06 -040031#include <media/text.h>
32#include <callmodel.h>
Stepan Salenikovich26cd1602016-01-20 13:43:17 -050033#include <media/textrecording.h>
34#include <media/recordingmodel.h>
Stepan Salenikovich76c33e62015-05-22 12:24:07 -040035#endif
36
37void
38ring_notify_init()
39{
40#if USE_LIBNOTIFY
41 notify_init("Ring");
42#endif
43}
44
45void
46ring_notify_uninit()
47{
48#if USE_LIBNOTIFY
49 if (notify_is_initted())
50 notify_uninit();
51#endif
52}
53
54gboolean
55ring_notify_is_initted()
56{
57#if USE_LIBNOTIFY
58 return notify_is_initted();
59#else
60 return FALSE;
61#endif
62}
63
64gboolean
65ring_notify_incoming_call(
66#if !USE_LIBNOTIFY
67 G_GNUC_UNUSED
68#endif
69 Call* call)
70{
Stepan Salenikovich9675db32015-06-16 14:36:54 -040071 gboolean success = FALSE;
Stepan Salenikovich76c33e62015-05-22 12:24:07 -040072#if USE_LIBNOTIFY
73 g_return_val_if_fail(call, FALSE);
74
75 gchar *body = g_strdup_printf("%s", call->formattedName().toUtf8().constData());
Stepan Salenikovich9675db32015-06-16 14:36:54 -040076 std::shared_ptr<NotifyNotification> notification(
Stepan Salenikovicha1b8cb32015-09-11 14:58:35 -040077 notify_notification_new(_("Incoming call"), body, NULL), g_object_unref);
Stepan Salenikovichc2e44262015-06-16 12:46:33 -040078 g_free(body);
Stepan Salenikovich76c33e62015-05-22 12:24:07 -040079
80 /* get photo */
Stepan Salenikovichbbd6c132015-08-20 15:21:48 -040081 QVariant var_p = GlobalInstances::pixmapManipulator().callPhoto(
Stepan Salenikovich76c33e62015-05-22 12:24:07 -040082 call->peerContactMethod(), QSize(50, 50), false);
83 std::shared_ptr<GdkPixbuf> photo = var_p.value<std::shared_ptr<GdkPixbuf>>();
Stepan Salenikovich9675db32015-06-16 14:36:54 -040084 notify_notification_set_image_from_pixbuf(notification.get(), photo.get());
Stepan Salenikovich76c33e62015-05-22 12:24:07 -040085
86 /* calls have highest urgency */
Stepan Salenikovich9675db32015-06-16 14:36:54 -040087 notify_notification_set_urgency(notification.get(), NOTIFY_URGENCY_CRITICAL);
88 notify_notification_set_timeout(notification.get(), NOTIFY_EXPIRES_DEFAULT);
Stepan Salenikovich76c33e62015-05-22 12:24:07 -040089
90 GError *error = NULL;
Stepan Salenikovich9675db32015-06-16 14:36:54 -040091 success = notify_notification_show(notification.get(), &error);
92
93 if (success) {
94 /* monitor the life cycle of the call and try to close the notification
95 * once the call has been aswered */
96 auto state_changed_conn = std::make_shared<QMetaObject::Connection>();
97 *state_changed_conn = QObject::connect(
98 call,
99 &Call::lifeCycleStateChanged,
100 [notification, state_changed_conn] (Call::LifeCycleState newState, G_GNUC_UNUSED Call::LifeCycleState previousState)
101 {
102 g_return_if_fail(NOTIFY_IS_NOTIFICATION(notification.get()));
103 if (newState > Call::LifeCycleState::INITIALIZATION) {
104 /* note: not all systems will actually close the notification
105 * even if the above function returns as true */
106 if (!notify_notification_close(notification.get(), NULL))
107 g_warning("could not close notification");
108
109 /* once we (try to) close the notification, we can
110 * disconnect from this signal; this should also destroy
111 * the notification shared_ptr as its ref count will
112 * drop to 0 */
113 QObject::disconnect(*state_changed_conn);
114 }
115 }
116 );
Stepan Salenikovich76c33e62015-05-22 12:24:07 -0400117 } else {
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400118 g_warning("failed to show notification: %s", error->message);
Stepan Salenikovich76c33e62015-05-22 12:24:07 -0400119 g_clear_error(&error);
Stepan Salenikovich76c33e62015-05-22 12:24:07 -0400120 }
Stepan Salenikovich76c33e62015-05-22 12:24:07 -0400121#endif
Stepan Salenikovich9675db32015-06-16 14:36:54 -0400122 return success;
Stepan Salenikovich76c33e62015-05-22 12:24:07 -0400123}
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400124
125#if USE_LIBNOTIFY
126
127GHashTable *
128ring_notify_get_chat_table()
129{
130 static std::unique_ptr<GHashTable, decltype(g_hash_table_destroy)&> chat_table(
131 nullptr, g_hash_table_destroy);
132
133 if (chat_table.get() == nullptr)
134 chat_table.reset(g_hash_table_new_full(NULL, NULL, NULL, g_object_unref));
135
136 return chat_table.get();
137}
138
139static void
Stepan Salenikovich26cd1602016-01-20 13:43:17 -0500140notification_closed(NotifyNotification *notification, ContactMethod *cm)
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400141{
Stepan Salenikovich26cd1602016-01-20 13:43:17 -0500142 g_return_if_fail(cm);
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400143
Stepan Salenikovich26cd1602016-01-20 13:43:17 -0500144 if (!g_hash_table_remove(ring_notify_get_chat_table(), cm)) {
145 g_warning("could not find notification associated with the given ContactMethod");
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400146 /* normally removing the notification from the hash table will unref it,
147 * but if it was not found we should do it here */
148 g_object_unref(notification);
149 }
150}
151
152static gboolean
Stepan Salenikovich26cd1602016-01-20 13:43:17 -0500153ring_notify_show_text_message(ContactMethod *cm, const QModelIndex& idx)
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400154{
Stepan Salenikovich26cd1602016-01-20 13:43:17 -0500155 g_return_val_if_fail(idx.isValid() && cm, FALSE);
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400156 gboolean success = FALSE;
157
158 GHashTable *chat_table = ring_notify_get_chat_table();
159
Stepan Salenikovich26cd1602016-01-20 13:43:17 -0500160 auto title = g_strdup_printf(C_("Text message notification", "%s says:"), idx.data(static_cast<int>(Ring::Role::Name)).toString().toUtf8().constData());
161 auto body = g_strdup_printf("%s", idx.data(Qt::DisplayRole).toString().toUtf8().constData());
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400162
Stepan Salenikovich26cd1602016-01-20 13:43:17 -0500163 /* check if a notification already exists for this CM */
164 NotifyNotification *notification = (NotifyNotification *)g_hash_table_lookup(chat_table, cm);
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400165 if (notification) {
166 /* update notification; append the new message to the old */
167 GValue body_value = G_VALUE_INIT;
168 g_value_init(&body_value, G_TYPE_STRING);
169 g_object_get_property(G_OBJECT(notification), "body", &body_value);
170 const gchar* body_old = g_value_get_string(&body_value);
171 if (body_old && (strlen(body_old) > 0)) {
172 gchar *body_new = g_strconcat(body_old, "\n", body, NULL);
173 g_free(body);
174 body = body_new;
175 }
176 notify_notification_update(notification, title, body, NULL);
177 } else {
Stepan Salenikovich26cd1602016-01-20 13:43:17 -0500178 /* create new notification object and associate it with the CM in the
179 * hash table; also store the pointer of the CM in the notification
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400180 * object so that it knows it's key in the hash table */
181 notification = notify_notification_new(title, body, NULL);
Stepan Salenikovich26cd1602016-01-20 13:43:17 -0500182 g_hash_table_insert(chat_table, cm, notification);
183 g_object_set_data(G_OBJECT(notification), "ContactMethod", cm);
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400184
185 /* get photo */
Stepan Salenikovichbbd6c132015-08-20 15:21:48 -0400186 QVariant var_p = GlobalInstances::pixmapManipulator().callPhoto(
Stepan Salenikovich26cd1602016-01-20 13:43:17 -0500187 cm, QSize(50, 50), false);
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400188 std::shared_ptr<GdkPixbuf> photo = var_p.value<std::shared_ptr<GdkPixbuf>>();
189 notify_notification_set_image_from_pixbuf(notification, photo.get());
190
191 /* normal priority for messages */
192 notify_notification_set_urgency(notification, NOTIFY_URGENCY_NORMAL);
193
194 /* remove the key and value from the hash table once the notification is
195 * closed; note that this will also unref the notification */
Stepan Salenikovich26cd1602016-01-20 13:43:17 -0500196 g_signal_connect(notification, "closed", G_CALLBACK(notification_closed), cm);
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400197 }
198
199 g_free(title);
200 g_free(body);
201
202 GError *error = NULL;
203 success = notify_notification_show(notification, &error);
204 if (!success) {
205 g_warning("failed to show notification: %s", error->message);
206 g_clear_error(&error);
Stepan Salenikovich26cd1602016-01-20 13:43:17 -0500207 g_hash_table_remove(chat_table, cm);
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400208 }
209
210 return success;
211}
212
Stepan Salenikovich26cd1602016-01-20 13:43:17 -0500213static gboolean
214show_message_if_unread(const QModelIndex *idx)
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400215{
Stepan Salenikovich26cd1602016-01-20 13:43:17 -0500216 g_return_val_if_fail(idx && idx->isValid(), G_SOURCE_REMOVE);
217
218 if (!idx->data(static_cast<int>(Media::TextRecording::Role::IsRead)).toBool()) {
219 auto cm = idx->data(static_cast<int>(Media::TextRecording::Role::ContactMethod)).value<ContactMethod *>();
220 ring_notify_show_text_message(cm, *idx);
221 }
222
223 return G_SOURCE_REMOVE;
224}
225
226static void
227delete_idx(QModelIndex *idx)
228{
229 delete idx;
230}
231
232static void
233ring_notify_message(ContactMethod *cm, Media::TextRecording *t, RingClient *client)
234{
235 g_return_if_fail(cm && t && client);
236
237 // get the message
238 auto model = t->instantMessagingModel();
239 auto msg_idx = model->index(model->rowCount()-1, 0);
240
241 // make sure its a text message, or else there is nothing to do
242 if (msg_idx.data(static_cast<int>(Media::TextRecording::Role::HasText)).toBool()) {
243 auto main_window = ring_client_get_main_window(client);
244 if ( main_window && gtk_window_is_active(main_window)) {
245 /* in this case we only want to show the notification if the message is not marked as
246 * read; this will only possibly be done after the the chatview has displayed it in
247 * response to this or another signal; so we must check for the read status after the
248 * chat view handler has completed, we do so via a g_idle function.
249 */
250 auto new_idx = new QModelIndex(msg_idx);
251 g_idle_add_full(G_PRIORITY_DEFAULT_IDLE, (GSourceFunc)show_message_if_unread, new_idx, (GDestroyNotify)delete_idx);
252 } else {
253 /* always show a notification if the window is not active/visible */
254 ring_notify_show_text_message(cm, msg_idx);
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400255 }
Stepan Salenikovich26cd1602016-01-20 13:43:17 -0500256
257 }
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400258}
259#endif
260
261void
262ring_notify_monitor_chat_notifications(
263#if !USE_LIBNOTIFY
264 G_GNUC_UNUSED
265#endif
266 RingClient *client)
267{
268#if USE_LIBNOTIFY
269
270 QObject::connect(
Stepan Salenikovich26cd1602016-01-20 13:43:17 -0500271 &Media::RecordingModel::instance(),
272 &Media::RecordingModel::newTextMessage,
273 [client] (Media::TextRecording* t, ContactMethod* cm)
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400274 {
Stepan Salenikovich26cd1602016-01-20 13:43:17 -0500275 ring_notify_message(cm, t, client);
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400276 }
277 );
278#endif
279 }
280
281gboolean
282ring_notify_close_chat_notification(
283#if !USE_LIBNOTIFY
284 G_GNUC_UNUSED
285#endif
Stepan Salenikovich26cd1602016-01-20 13:43:17 -0500286 ContactMethod *cm)
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400287{
288 gboolean notification_existed = FALSE;
289
290#if USE_LIBNOTIFY
Stepan Salenikovich26cd1602016-01-20 13:43:17 -0500291 /* checks if there exists a chat notification associated with the given ContactMethod
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400292 * and tries to close it; if it did exist, then the function returns TRUE */
Stepan Salenikovich26cd1602016-01-20 13:43:17 -0500293 g_return_val_if_fail(cm, FALSE);
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400294
295
296 GHashTable *chat_table = ring_notify_get_chat_table();
297
Stepan Salenikovich26cd1602016-01-20 13:43:17 -0500298 NotifyNotification *notification = (NotifyNotification *)g_hash_table_lookup(chat_table, cm);
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400299
300 if (notification) {
301 notification_existed = TRUE;
302
303 GError *error = NULL;
304 if (!notify_notification_close(notification, &error)) {
305 g_warning("could not close notification: %s", error->message);
306 g_clear_error(&error);
307
308 /* closing should remove and free the notification from the hash table
309 * since it failed to close, try to remove the notification from the
310 * table manually */
Stepan Salenikovich26cd1602016-01-20 13:43:17 -0500311 g_hash_table_remove(chat_table, cm);
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400312 }
313 }
314#endif
315
316 return notification_existed;
317}