Stepan Salenikovich | 76c33e6 | 2015-05-22 12:24:07 -0400 | [diff] [blame] | 1 | /* |
Stepan Salenikovich | be87d2c | 2016-01-25 14:14:34 -0500 | [diff] [blame] | 2 | * Copyright (C) 2015-2016 Savoir-faire Linux Inc. |
Stepan Salenikovich | 76c33e6 | 2015-05-22 12:24:07 -0400 | [diff] [blame] | 3 | * 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 Salenikovich | 76c33e6 | 2015-05-22 12:24:07 -0400 | [diff] [blame] | 18 | */ |
| 19 | |
| 20 | #include "ringnotify.h" |
| 21 | #include "config.h" |
Stepan Salenikovich | 5834f83 | 2016-05-20 15:32:29 -0400 | [diff] [blame] | 22 | #include "ring_client.h" |
Stepan Salenikovich | 76c33e6 | 2015-05-22 12:24:07 -0400 | [diff] [blame] | 23 | |
| 24 | #if USE_LIBNOTIFY |
Stepan Salenikovich | a1b8cb3 | 2015-09-11 14:58:35 -0400 | [diff] [blame] | 25 | #include <glib/gi18n.h> |
Stepan Salenikovich | 76c33e6 | 2015-05-22 12:24:07 -0400 | [diff] [blame] | 26 | #include <libnotify/notify.h> |
| 27 | #include <memory> |
Stepan Salenikovich | bbd6c13 | 2015-08-20 15:21:48 -0400 | [diff] [blame] | 28 | #include <globalinstances.h> |
| 29 | #include "native/pixbufmanipulator.h" |
Stepan Salenikovich | 76c33e6 | 2015-05-22 12:24:07 -0400 | [diff] [blame] | 30 | #include <call.h> |
| 31 | #include <QtCore/QSize> |
Stepan Salenikovich | 67112d1 | 2015-06-16 16:57:06 -0400 | [diff] [blame] | 32 | #include <media/text.h> |
| 33 | #include <callmodel.h> |
Stepan Salenikovich | 26cd160 | 2016-01-20 13:43:17 -0500 | [diff] [blame] | 34 | #include <media/textrecording.h> |
| 35 | #include <media/recordingmodel.h> |
Stepan Salenikovich | 76c33e6 | 2015-05-22 12:24:07 -0400 | [diff] [blame] | 36 | #endif |
| 37 | |
| 38 | void |
| 39 | ring_notify_init() |
| 40 | { |
| 41 | #if USE_LIBNOTIFY |
| 42 | notify_init("Ring"); |
| 43 | #endif |
| 44 | } |
| 45 | |
| 46 | void |
| 47 | ring_notify_uninit() |
| 48 | { |
| 49 | #if USE_LIBNOTIFY |
| 50 | if (notify_is_initted()) |
| 51 | notify_uninit(); |
| 52 | #endif |
| 53 | } |
| 54 | |
| 55 | gboolean |
| 56 | ring_notify_is_initted() |
| 57 | { |
| 58 | #if USE_LIBNOTIFY |
| 59 | return notify_is_initted(); |
| 60 | #else |
| 61 | return FALSE; |
| 62 | #endif |
| 63 | } |
| 64 | |
| 65 | gboolean |
| 66 | ring_notify_incoming_call( |
| 67 | #if !USE_LIBNOTIFY |
| 68 | G_GNUC_UNUSED |
| 69 | #endif |
| 70 | Call* call) |
| 71 | { |
Stepan Salenikovich | 9675db3 | 2015-06-16 14:36:54 -0400 | [diff] [blame] | 72 | gboolean success = FALSE; |
Stepan Salenikovich | 76c33e6 | 2015-05-22 12:24:07 -0400 | [diff] [blame] | 73 | #if USE_LIBNOTIFY |
| 74 | g_return_val_if_fail(call, FALSE); |
| 75 | |
| 76 | gchar *body = g_strdup_printf("%s", call->formattedName().toUtf8().constData()); |
Stepan Salenikovich | 9675db3 | 2015-06-16 14:36:54 -0400 | [diff] [blame] | 77 | std::shared_ptr<NotifyNotification> notification( |
Stepan Salenikovich | a1b8cb3 | 2015-09-11 14:58:35 -0400 | [diff] [blame] | 78 | notify_notification_new(_("Incoming call"), body, NULL), g_object_unref); |
Stepan Salenikovich | c2e4426 | 2015-06-16 12:46:33 -0400 | [diff] [blame] | 79 | g_free(body); |
Stepan Salenikovich | 76c33e6 | 2015-05-22 12:24:07 -0400 | [diff] [blame] | 80 | |
| 81 | /* get photo */ |
Stepan Salenikovich | bbd6c13 | 2015-08-20 15:21:48 -0400 | [diff] [blame] | 82 | QVariant var_p = GlobalInstances::pixmapManipulator().callPhoto( |
Stepan Salenikovich | 76c33e6 | 2015-05-22 12:24:07 -0400 | [diff] [blame] | 83 | call->peerContactMethod(), QSize(50, 50), false); |
| 84 | std::shared_ptr<GdkPixbuf> photo = var_p.value<std::shared_ptr<GdkPixbuf>>(); |
Stepan Salenikovich | 9675db3 | 2015-06-16 14:36:54 -0400 | [diff] [blame] | 85 | notify_notification_set_image_from_pixbuf(notification.get(), photo.get()); |
Stepan Salenikovich | 76c33e6 | 2015-05-22 12:24:07 -0400 | [diff] [blame] | 86 | |
| 87 | /* calls have highest urgency */ |
Stepan Salenikovich | 9675db3 | 2015-06-16 14:36:54 -0400 | [diff] [blame] | 88 | notify_notification_set_urgency(notification.get(), NOTIFY_URGENCY_CRITICAL); |
| 89 | notify_notification_set_timeout(notification.get(), NOTIFY_EXPIRES_DEFAULT); |
Stepan Salenikovich | 76c33e6 | 2015-05-22 12:24:07 -0400 | [diff] [blame] | 90 | |
| 91 | GError *error = NULL; |
Stepan Salenikovich | 9675db3 | 2015-06-16 14:36:54 -0400 | [diff] [blame] | 92 | 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 Salenikovich | 76c33e6 | 2015-05-22 12:24:07 -0400 | [diff] [blame] | 118 | } else { |
Stepan Salenikovich | 67112d1 | 2015-06-16 16:57:06 -0400 | [diff] [blame] | 119 | g_warning("failed to show notification: %s", error->message); |
Stepan Salenikovich | 76c33e6 | 2015-05-22 12:24:07 -0400 | [diff] [blame] | 120 | g_clear_error(&error); |
Stepan Salenikovich | 76c33e6 | 2015-05-22 12:24:07 -0400 | [diff] [blame] | 121 | } |
Stepan Salenikovich | 76c33e6 | 2015-05-22 12:24:07 -0400 | [diff] [blame] | 122 | #endif |
Stepan Salenikovich | 9675db3 | 2015-06-16 14:36:54 -0400 | [diff] [blame] | 123 | return success; |
Stepan Salenikovich | 76c33e6 | 2015-05-22 12:24:07 -0400 | [diff] [blame] | 124 | } |
Stepan Salenikovich | 67112d1 | 2015-06-16 16:57:06 -0400 | [diff] [blame] | 125 | |
| 126 | #if USE_LIBNOTIFY |
| 127 | |
| 128 | GHashTable * |
| 129 | ring_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 | |
| 140 | static void |
Stepan Salenikovich | 26cd160 | 2016-01-20 13:43:17 -0500 | [diff] [blame] | 141 | notification_closed(NotifyNotification *notification, ContactMethod *cm) |
Stepan Salenikovich | 67112d1 | 2015-06-16 16:57:06 -0400 | [diff] [blame] | 142 | { |
Stepan Salenikovich | 26cd160 | 2016-01-20 13:43:17 -0500 | [diff] [blame] | 143 | g_return_if_fail(cm); |
Stepan Salenikovich | 67112d1 | 2015-06-16 16:57:06 -0400 | [diff] [blame] | 144 | |
Stepan Salenikovich | 26cd160 | 2016-01-20 13:43:17 -0500 | [diff] [blame] | 145 | if (!g_hash_table_remove(ring_notify_get_chat_table(), cm)) { |
| 146 | g_warning("could not find notification associated with the given ContactMethod"); |
Stepan Salenikovich | 67112d1 | 2015-06-16 16:57:06 -0400 | [diff] [blame] | 147 | /* 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 | |
| 153 | static gboolean |
Stepan Salenikovich | 26cd160 | 2016-01-20 13:43:17 -0500 | [diff] [blame] | 154 | ring_notify_show_text_message(ContactMethod *cm, const QModelIndex& idx) |
Stepan Salenikovich | 67112d1 | 2015-06-16 16:57:06 -0400 | [diff] [blame] | 155 | { |
Stepan Salenikovich | 26cd160 | 2016-01-20 13:43:17 -0500 | [diff] [blame] | 156 | g_return_val_if_fail(idx.isValid() && cm, FALSE); |
Stepan Salenikovich | 67112d1 | 2015-06-16 16:57:06 -0400 | [diff] [blame] | 157 | gboolean success = FALSE; |
| 158 | |
| 159 | GHashTable *chat_table = ring_notify_get_chat_table(); |
| 160 | |
Stepan Salenikovich | 26cd160 | 2016-01-20 13:43:17 -0500 | [diff] [blame] | 161 | 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 Salenikovich | 67112d1 | 2015-06-16 16:57:06 -0400 | [diff] [blame] | 163 | |
Stepan Salenikovich | 26cd160 | 2016-01-20 13:43:17 -0500 | [diff] [blame] | 164 | /* check if a notification already exists for this CM */ |
| 165 | NotifyNotification *notification = (NotifyNotification *)g_hash_table_lookup(chat_table, cm); |
Stepan Salenikovich | 67112d1 | 2015-06-16 16:57:06 -0400 | [diff] [blame] | 166 | 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 Salenikovich | 26cd160 | 2016-01-20 13:43:17 -0500 | [diff] [blame] | 179 | /* 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 Salenikovich | 67112d1 | 2015-06-16 16:57:06 -0400 | [diff] [blame] | 181 | * object so that it knows it's key in the hash table */ |
| 182 | notification = notify_notification_new(title, body, NULL); |
Stepan Salenikovich | 26cd160 | 2016-01-20 13:43:17 -0500 | [diff] [blame] | 183 | g_hash_table_insert(chat_table, cm, notification); |
| 184 | g_object_set_data(G_OBJECT(notification), "ContactMethod", cm); |
Stepan Salenikovich | 67112d1 | 2015-06-16 16:57:06 -0400 | [diff] [blame] | 185 | |
| 186 | /* get photo */ |
Stepan Salenikovich | bbd6c13 | 2015-08-20 15:21:48 -0400 | [diff] [blame] | 187 | QVariant var_p = GlobalInstances::pixmapManipulator().callPhoto( |
Stepan Salenikovich | 26cd160 | 2016-01-20 13:43:17 -0500 | [diff] [blame] | 188 | cm, QSize(50, 50), false); |
Stepan Salenikovich | 67112d1 | 2015-06-16 16:57:06 -0400 | [diff] [blame] | 189 | 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 Salenikovich | 26cd160 | 2016-01-20 13:43:17 -0500 | [diff] [blame] | 197 | g_signal_connect(notification, "closed", G_CALLBACK(notification_closed), cm); |
Stepan Salenikovich | 67112d1 | 2015-06-16 16:57:06 -0400 | [diff] [blame] | 198 | } |
| 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 Salenikovich | 26cd160 | 2016-01-20 13:43:17 -0500 | [diff] [blame] | 208 | g_hash_table_remove(chat_table, cm); |
Stepan Salenikovich | 67112d1 | 2015-06-16 16:57:06 -0400 | [diff] [blame] | 209 | } |
| 210 | |
| 211 | return success; |
| 212 | } |
| 213 | |
Stepan Salenikovich | 26cd160 | 2016-01-20 13:43:17 -0500 | [diff] [blame] | 214 | static gboolean |
| 215 | show_message_if_unread(const QModelIndex *idx) |
Stepan Salenikovich | 67112d1 | 2015-06-16 16:57:06 -0400 | [diff] [blame] | 216 | { |
Stepan Salenikovich | 26cd160 | 2016-01-20 13:43:17 -0500 | [diff] [blame] | 217 | 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 | |
| 227 | static void |
| 228 | delete_idx(QModelIndex *idx) |
| 229 | { |
| 230 | delete idx; |
| 231 | } |
| 232 | |
| 233 | static void |
| 234 | ring_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 Salenikovich | 67112d1 | 2015-06-16 16:57:06 -0400 | [diff] [blame] | 256 | } |
Stepan Salenikovich | 26cd160 | 2016-01-20 13:43:17 -0500 | [diff] [blame] | 257 | |
| 258 | } |
Stepan Salenikovich | 67112d1 | 2015-06-16 16:57:06 -0400 | [diff] [blame] | 259 | } |
| 260 | #endif |
| 261 | |
| 262 | void |
| 263 | ring_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 Salenikovich | 26cd160 | 2016-01-20 13:43:17 -0500 | [diff] [blame] | 272 | &Media::RecordingModel::instance(), |
| 273 | &Media::RecordingModel::newTextMessage, |
| 274 | [client] (Media::TextRecording* t, ContactMethod* cm) |
Stepan Salenikovich | 67112d1 | 2015-06-16 16:57:06 -0400 | [diff] [blame] | 275 | { |
Stepan Salenikovich | 26cd160 | 2016-01-20 13:43:17 -0500 | [diff] [blame] | 276 | ring_notify_message(cm, t, client); |
Stepan Salenikovich | 67112d1 | 2015-06-16 16:57:06 -0400 | [diff] [blame] | 277 | } |
| 278 | ); |
| 279 | #endif |
| 280 | } |
| 281 | |
| 282 | gboolean |
| 283 | ring_notify_close_chat_notification( |
| 284 | #if !USE_LIBNOTIFY |
| 285 | G_GNUC_UNUSED |
| 286 | #endif |
Stepan Salenikovich | 26cd160 | 2016-01-20 13:43:17 -0500 | [diff] [blame] | 287 | ContactMethod *cm) |
Stepan Salenikovich | 67112d1 | 2015-06-16 16:57:06 -0400 | [diff] [blame] | 288 | { |
| 289 | gboolean notification_existed = FALSE; |
| 290 | |
| 291 | #if USE_LIBNOTIFY |
Stepan Salenikovich | 26cd160 | 2016-01-20 13:43:17 -0500 | [diff] [blame] | 292 | /* checks if there exists a chat notification associated with the given ContactMethod |
Stepan Salenikovich | 67112d1 | 2015-06-16 16:57:06 -0400 | [diff] [blame] | 293 | * and tries to close it; if it did exist, then the function returns TRUE */ |
Stepan Salenikovich | 26cd160 | 2016-01-20 13:43:17 -0500 | [diff] [blame] | 294 | g_return_val_if_fail(cm, FALSE); |
Stepan Salenikovich | 67112d1 | 2015-06-16 16:57:06 -0400 | [diff] [blame] | 295 | |
| 296 | |
| 297 | GHashTable *chat_table = ring_notify_get_chat_table(); |
| 298 | |
Stepan Salenikovich | 26cd160 | 2016-01-20 13:43:17 -0500 | [diff] [blame] | 299 | NotifyNotification *notification = (NotifyNotification *)g_hash_table_lookup(chat_table, cm); |
Stepan Salenikovich | 67112d1 | 2015-06-16 16:57:06 -0400 | [diff] [blame] | 300 | |
| 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 Salenikovich | 26cd160 | 2016-01-20 13:43:17 -0500 | [diff] [blame] | 312 | g_hash_table_remove(chat_table, cm); |
Stepan Salenikovich | 67112d1 | 2015-06-16 16:57:06 -0400 | [diff] [blame] | 313 | } |
| 314 | } |
| 315 | #endif |
| 316 | |
| 317 | return notification_existed; |
| 318 | } |