1package eu.siacs.conversations.services;
2
3import android.app.Notification;
4import android.app.NotificationChannel;
5import android.app.NotificationChannelGroup;
6import android.app.NotificationManager;
7import android.app.PendingIntent;
8import android.content.Context;
9import android.content.Intent;
10import android.content.SharedPreferences;
11import android.content.res.Resources;
12import android.graphics.Bitmap;
13import android.graphics.Typeface;
14import android.graphics.drawable.Icon;
15import android.media.AudioAttributes;
16import android.media.RingtoneManager;
17import android.net.Uri;
18import android.os.Build;
19import android.os.SystemClock;
20import android.preference.PreferenceManager;
21import android.support.annotation.RequiresApi;
22import android.support.v4.app.NotificationCompat;
23import android.support.v4.app.NotificationCompat.BigPictureStyle;
24import android.support.v4.app.NotificationCompat.Builder;
25import android.support.v4.app.NotificationManagerCompat;
26import android.support.v4.app.NotificationCompat.CarExtender.UnreadConversation;
27import android.support.v4.app.Person;
28import android.support.v4.app.RemoteInput;
29import android.support.v4.content.ContextCompat;
30import android.support.v4.graphics.drawable.IconCompat;
31import android.text.SpannableString;
32import android.text.style.StyleSpan;
33import android.util.DisplayMetrics;
34import android.util.Log;
35import android.util.Pair;
36
37import java.io.File;
38import java.io.IOException;
39import java.util.ArrayList;
40import java.util.Calendar;
41import java.util.Collections;
42import java.util.HashMap;
43import java.util.Iterator;
44import java.util.LinkedHashMap;
45import java.util.List;
46import java.util.Map;
47import java.util.concurrent.atomic.AtomicInteger;
48import java.util.regex.Matcher;
49import java.util.regex.Pattern;
50
51import eu.siacs.conversations.Config;
52import eu.siacs.conversations.R;
53import eu.siacs.conversations.entities.Account;
54import eu.siacs.conversations.entities.Contact;
55import eu.siacs.conversations.entities.Conversation;
56import eu.siacs.conversations.entities.Conversational;
57import eu.siacs.conversations.entities.Message;
58import eu.siacs.conversations.persistance.FileBackend;
59import eu.siacs.conversations.ui.ConversationsActivity;
60import eu.siacs.conversations.ui.EditAccountActivity;
61import eu.siacs.conversations.ui.TimePreference;
62import eu.siacs.conversations.utils.AccountUtils;
63import eu.siacs.conversations.utils.Compatibility;
64import eu.siacs.conversations.utils.GeoHelper;
65import eu.siacs.conversations.utils.UIHelper;
66import eu.siacs.conversations.xmpp.XmppConnection;
67import rocks.xmpp.addr.Jid;
68
69public class NotificationService {
70
71 public static final Object CATCHUP_LOCK = new Object();
72
73 private static final int LED_COLOR = 0xff00ff00;
74
75 private static final String CONVERSATIONS_GROUP = "eu.siacs.conversations";
76 private static final int NOTIFICATION_ID_MULTIPLIER = 1024 * 1024;
77 private static final int NOTIFICATION_ID = 2 * NOTIFICATION_ID_MULTIPLIER;
78 static final int FOREGROUND_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 4;
79 private static final int ERROR_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 6;
80 private final XmppConnectionService mXmppConnectionService;
81 private final LinkedHashMap<String, ArrayList<Message>> notifications = new LinkedHashMap<>();
82 private final HashMap<Conversation, AtomicInteger> mBacklogMessageCounter = new HashMap<>();
83 private Conversation mOpenConversation;
84 private boolean mIsInForeground;
85 private long mLastNotification;
86
87 NotificationService(final XmppConnectionService service) {
88 this.mXmppConnectionService = service;
89 }
90
91 private static boolean displaySnoozeAction(List<Message> messages) {
92 int numberOfMessagesWithoutReply = 0;
93 for (Message message : messages) {
94 if (message.getStatus() == Message.STATUS_RECEIVED) {
95 ++numberOfMessagesWithoutReply;
96 } else {
97 return false;
98 }
99 }
100 return numberOfMessagesWithoutReply >= 3;
101 }
102
103 public static Pattern generateNickHighlightPattern(final String nick) {
104 return Pattern.compile("(?<=(^|\\s))" + Pattern.quote(nick) + "(?=\\s|$|\\p{Punct})");
105 }
106
107 @RequiresApi(api = Build.VERSION_CODES.O)
108 void initializeChannels() {
109 final Context c = mXmppConnectionService;
110 final NotificationManager notificationManager = c.getSystemService(NotificationManager.class);
111 if (notificationManager == null) {
112 return;
113 }
114
115 notificationManager.deleteNotificationChannel("export");
116
117 notificationManager.createNotificationChannelGroup(new NotificationChannelGroup("status", c.getString(R.string.notification_group_status_information)));
118 notificationManager.createNotificationChannelGroup(new NotificationChannelGroup("chats", c.getString(R.string.notification_group_messages)));
119 final NotificationChannel foregroundServiceChannel = new NotificationChannel("foreground",
120 c.getString(R.string.foreground_service_channel_name),
121 NotificationManager.IMPORTANCE_MIN);
122 foregroundServiceChannel.setDescription(c.getString(R.string.foreground_service_channel_description));
123 foregroundServiceChannel.setShowBadge(false);
124 foregroundServiceChannel.setGroup("status");
125 notificationManager.createNotificationChannel(foregroundServiceChannel);
126 final NotificationChannel errorChannel = new NotificationChannel("error",
127 c.getString(R.string.error_channel_name),
128 NotificationManager.IMPORTANCE_LOW);
129 errorChannel.setDescription(c.getString(R.string.error_channel_description));
130 errorChannel.setShowBadge(false);
131 errorChannel.setGroup("status");
132 notificationManager.createNotificationChannel(errorChannel);
133
134 final NotificationChannel videoCompressionChannel = new NotificationChannel("compression",
135 c.getString(R.string.video_compression_channel_name),
136 NotificationManager.IMPORTANCE_LOW);
137 videoCompressionChannel.setShowBadge(false);
138 videoCompressionChannel.setGroup("status");
139 notificationManager.createNotificationChannel(videoCompressionChannel);
140
141 final NotificationChannel exportChannel = new NotificationChannel("backup",
142 c.getString(R.string.backup_channel_name),
143 NotificationManager.IMPORTANCE_LOW);
144 exportChannel.setShowBadge(false);
145 exportChannel.setGroup("status");
146 notificationManager.createNotificationChannel(exportChannel);
147
148 final NotificationChannel messagesChannel = new NotificationChannel("messages",
149 c.getString(R.string.messages_channel_name),
150 NotificationManager.IMPORTANCE_HIGH);
151 messagesChannel.setShowBadge(true);
152 messagesChannel.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION), new AudioAttributes.Builder()
153 .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
154 .setUsage(AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT)
155 .build());
156 messagesChannel.setLightColor(LED_COLOR);
157 final int dat = 70;
158 final long[] pattern = {0, 3 * dat, dat, dat};
159 messagesChannel.setVibrationPattern(pattern);
160 messagesChannel.enableVibration(true);
161 messagesChannel.enableLights(true);
162 messagesChannel.setGroup("chats");
163 notificationManager.createNotificationChannel(messagesChannel);
164 final NotificationChannel silentMessagesChannel = new NotificationChannel("silent_messages",
165 c.getString(R.string.silent_messages_channel_name),
166 NotificationManager.IMPORTANCE_LOW);
167 silentMessagesChannel.setDescription(c.getString(R.string.silent_messages_channel_description));
168 silentMessagesChannel.setShowBadge(true);
169 silentMessagesChannel.setLightColor(LED_COLOR);
170 silentMessagesChannel.enableLights(true);
171 silentMessagesChannel.setGroup("chats");
172 notificationManager.createNotificationChannel(silentMessagesChannel);
173
174 final NotificationChannel quietHoursChannel = new NotificationChannel("quiet_hours",
175 c.getString(R.string.title_pref_quiet_hours),
176 NotificationManager.IMPORTANCE_LOW);
177 quietHoursChannel.setShowBadge(true);
178 quietHoursChannel.setLightColor(LED_COLOR);
179 quietHoursChannel.enableLights(true);
180 quietHoursChannel.setGroup("chats");
181 quietHoursChannel.enableVibration(false);
182 quietHoursChannel.setSound(null, null);
183
184 notificationManager.createNotificationChannel(quietHoursChannel);
185 }
186
187 public boolean notify(final Message message) {
188 final Conversation conversation = (Conversation) message.getConversation();
189 return message.getStatus() == Message.STATUS_RECEIVED
190 && !conversation.isMuted()
191 && (conversation.alwaysNotify() || wasHighlightedOrPrivate(message))
192 && (!conversation.isWithStranger() || notificationsFromStrangers());
193 }
194
195 private boolean notificationsFromStrangers() {
196 return mXmppConnectionService.getBooleanPreference("notifications_from_strangers", R.bool.notifications_from_strangers);
197 }
198
199 private boolean isQuietHours() {
200 if (!mXmppConnectionService.getBooleanPreference("enable_quiet_hours", R.bool.enable_quiet_hours)) {
201 return false;
202 }
203 final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mXmppConnectionService);
204 final long startTime = TimePreference.minutesToTimestamp(preferences.getLong("quiet_hours_start", TimePreference.DEFAULT_VALUE));
205 final long endTime = TimePreference.minutesToTimestamp(preferences.getLong("quiet_hours_end", TimePreference.DEFAULT_VALUE));
206 final long nowTime = Calendar.getInstance().getTimeInMillis();
207
208 if (endTime < startTime) {
209 return nowTime > startTime || nowTime < endTime;
210 } else {
211 return nowTime > startTime && nowTime < endTime;
212 }
213 }
214
215 public void pushFromBacklog(final Message message) {
216 if (notify(message)) {
217 synchronized (notifications) {
218 getBacklogMessageCounter((Conversation) message.getConversation()).incrementAndGet();
219 pushToStack(message);
220 }
221 }
222 }
223
224 private AtomicInteger getBacklogMessageCounter(Conversation conversation) {
225 synchronized (mBacklogMessageCounter) {
226 if (!mBacklogMessageCounter.containsKey(conversation)) {
227 mBacklogMessageCounter.put(conversation, new AtomicInteger(0));
228 }
229 return mBacklogMessageCounter.get(conversation);
230 }
231 }
232
233 void pushFromDirectReply(final Message message) {
234 synchronized (notifications) {
235 pushToStack(message);
236 updateNotification(false);
237 }
238 }
239
240 public void finishBacklog(boolean notify, Account account) {
241 synchronized (notifications) {
242 mXmppConnectionService.updateUnreadCountBadge();
243 if (account == null || !notify) {
244 updateNotification(notify);
245 } else {
246 final int count;
247 final List<String> conversations;
248 synchronized (this.mBacklogMessageCounter) {
249 conversations = getBacklogConversations(account);
250 count = getBacklogMessageCount(account);
251 }
252 updateNotification(count > 0, conversations);
253 }
254 }
255 }
256
257 private List<String> getBacklogConversations(Account account) {
258 final List<String> conversations = new ArrayList<>();
259 for (Map.Entry<Conversation, AtomicInteger> entry : mBacklogMessageCounter.entrySet()) {
260 if (entry.getKey().getAccount() == account) {
261 conversations.add(entry.getKey().getUuid());
262 }
263 }
264 return conversations;
265 }
266
267 private int getBacklogMessageCount(Account account) {
268 int count = 0;
269 for (Iterator<Map.Entry<Conversation, AtomicInteger>> it = mBacklogMessageCounter.entrySet().iterator(); it.hasNext(); ) {
270 Map.Entry<Conversation, AtomicInteger> entry = it.next();
271 if (entry.getKey().getAccount() == account) {
272 count += entry.getValue().get();
273 it.remove();
274 }
275 }
276 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": backlog message count=" + count);
277 return count;
278 }
279
280 void finishBacklog(boolean notify) {
281 finishBacklog(notify, null);
282 }
283
284 private void pushToStack(final Message message) {
285 final String conversationUuid = message.getConversationUuid();
286 if (notifications.containsKey(conversationUuid)) {
287 notifications.get(conversationUuid).add(message);
288 } else {
289 final ArrayList<Message> mList = new ArrayList<>();
290 mList.add(message);
291 notifications.put(conversationUuid, mList);
292 }
293 }
294
295 public void push(final Message message) {
296 synchronized (CATCHUP_LOCK) {
297 final XmppConnection connection = message.getConversation().getAccount().getXmppConnection();
298 if (connection != null && connection.isWaitingForSmCatchup()) {
299 connection.incrementSmCatchupMessageCounter();
300 pushFromBacklog(message);
301 } else {
302 pushNow(message);
303 }
304 }
305 }
306
307 private void pushNow(final Message message) {
308 mXmppConnectionService.updateUnreadCountBadge();
309 if (!notify(message)) {
310 Log.d(Config.LOGTAG, message.getConversation().getAccount().getJid().asBareJid() + ": suppressing notification because turned off");
311 return;
312 }
313 final boolean isScreenOn = mXmppConnectionService.isInteractive();
314 if (this.mIsInForeground && isScreenOn && this.mOpenConversation == message.getConversation()) {
315 Log.d(Config.LOGTAG, message.getConversation().getAccount().getJid().asBareJid() + ": suppressing notification because conversation is open");
316 return;
317 }
318 synchronized (notifications) {
319 pushToStack(message);
320 final Conversational conversation = message.getConversation();
321 final Account account = conversation.getAccount();
322 final boolean doNotify = (!(this.mIsInForeground && this.mOpenConversation == null) || !isScreenOn)
323 && !account.inGracePeriod()
324 && !this.inMiniGracePeriod(account);
325 updateNotification(doNotify, Collections.singletonList(conversation.getUuid()));
326 }
327 }
328
329 public void clear() {
330 synchronized (notifications) {
331 for (ArrayList<Message> messages : notifications.values()) {
332 markAsReadIfHasDirectReply(messages);
333 }
334 notifications.clear();
335 updateNotification(false);
336 }
337 }
338
339 public void clear(final Conversation conversation) {
340 synchronized (this.mBacklogMessageCounter) {
341 this.mBacklogMessageCounter.remove(conversation);
342 }
343 synchronized (notifications) {
344 markAsReadIfHasDirectReply(conversation);
345 if (notifications.remove(conversation.getUuid()) != null) {
346 cancel(conversation.getUuid(), NOTIFICATION_ID);
347 updateNotification(false, null, true);
348 }
349 }
350 }
351
352 private void markAsReadIfHasDirectReply(final Conversation conversation) {
353 markAsReadIfHasDirectReply(notifications.get(conversation.getUuid()));
354 }
355
356 private void markAsReadIfHasDirectReply(final ArrayList<Message> messages) {
357 if (messages != null && messages.size() > 0) {
358 Message last = messages.get(messages.size() - 1);
359 if (last.getStatus() != Message.STATUS_RECEIVED) {
360 if (mXmppConnectionService.markRead((Conversation) last.getConversation(), false)) {
361 mXmppConnectionService.updateConversationUi();
362 }
363 }
364 }
365 }
366
367 private void setNotificationColor(final Builder mBuilder) {
368 mBuilder.setColor(ContextCompat.getColor(mXmppConnectionService, R.color.green600));
369 }
370
371 public void updateNotification() {
372 synchronized (notifications) {
373 updateNotification(false);
374 }
375 }
376
377 private void updateNotification(final boolean notify) {
378 updateNotification(notify, null, false);
379 }
380
381 private void updateNotification(final boolean notify, final List<String> conversations) {
382 updateNotification(notify, conversations, false);
383 }
384
385 private void updateNotification(final boolean notify, final List<String> conversations, final boolean summaryOnly) {
386 final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mXmppConnectionService);
387
388 final boolean quiteHours = isQuietHours();
389
390 final boolean notifyOnlyOneChild = notify && conversations != null && conversations.size() == 1; //if this check is changed to > 0 catchup messages will create one notification per conversation
391
392
393 if (notifications.size() == 0) {
394 cancel(NOTIFICATION_ID);
395 } else {
396 if (notify) {
397 this.markLastNotification();
398 }
399 final Builder mBuilder;
400 if (notifications.size() == 1 && Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
401 mBuilder = buildSingleConversations(notifications.values().iterator().next(), notify, quiteHours);
402 modifyForSoundVibrationAndLight(mBuilder, notify, quiteHours, preferences);
403 notify(NOTIFICATION_ID, mBuilder.build());
404 } else {
405 mBuilder = buildMultipleConversation(notify, quiteHours);
406 if (notifyOnlyOneChild) {
407 mBuilder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN);
408 }
409 modifyForSoundVibrationAndLight(mBuilder, notify, quiteHours, preferences);
410 if (!summaryOnly) {
411 for (Map.Entry<String, ArrayList<Message>> entry : notifications.entrySet()) {
412 String uuid = entry.getKey();
413 final boolean notifyThis = notifyOnlyOneChild ? conversations.contains(uuid) : notify;
414 Builder singleBuilder = buildSingleConversations(entry.getValue(), notifyThis, quiteHours);
415 if (!notifyOnlyOneChild) {
416 singleBuilder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY);
417 }
418 modifyForSoundVibrationAndLight(singleBuilder, notifyThis, quiteHours, preferences);
419 singleBuilder.setGroup(CONVERSATIONS_GROUP);
420 setNotificationColor(singleBuilder);
421 notify(entry.getKey(), NOTIFICATION_ID, singleBuilder.build());
422 }
423 }
424 notify(NOTIFICATION_ID, mBuilder.build());
425 }
426 }
427 }
428
429 private void modifyForSoundVibrationAndLight(Builder mBuilder, boolean notify, boolean quietHours, SharedPreferences preferences) {
430 final Resources resources = mXmppConnectionService.getResources();
431 final String ringtone = preferences.getString("notification_ringtone", resources.getString(R.string.notification_ringtone));
432 final boolean vibrate = preferences.getBoolean("vibrate_on_notification", resources.getBoolean(R.bool.vibrate_on_notification));
433 final boolean led = preferences.getBoolean("led", resources.getBoolean(R.bool.led));
434 final boolean headsup = preferences.getBoolean("notification_headsup", resources.getBoolean(R.bool.headsup_notifications));
435 if (notify && !quietHours) {
436 if (vibrate) {
437 final int dat = 70;
438 final long[] pattern = {0, 3 * dat, dat, dat};
439 mBuilder.setVibrate(pattern);
440 } else {
441 mBuilder.setVibrate(new long[]{0});
442 }
443 Uri uri = Uri.parse(ringtone);
444 try {
445 mBuilder.setSound(fixRingtoneUri(uri));
446 } catch (SecurityException e) {
447 Log.d(Config.LOGTAG, "unable to use custom notification sound " + uri.toString());
448 }
449 }
450 if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
451 mBuilder.setCategory(Notification.CATEGORY_MESSAGE);
452 }
453 mBuilder.setPriority(notify ? (headsup ? NotificationCompat.PRIORITY_HIGH : NotificationCompat.PRIORITY_DEFAULT) : NotificationCompat.PRIORITY_LOW);
454 setNotificationColor(mBuilder);
455 mBuilder.setDefaults(0);
456 if (led) {
457 mBuilder.setLights(LED_COLOR, 2000, 3000);
458 }
459 }
460
461 private Uri fixRingtoneUri(Uri uri) {
462 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && "file".equals(uri.getScheme())) {
463 return FileBackend.getUriForFile(mXmppConnectionService, new File(uri.getPath()));
464 } else {
465 return uri;
466 }
467 }
468
469 private Builder buildMultipleConversation(final boolean notify, final boolean quietHours) {
470 final Builder mBuilder = new NotificationCompat.Builder(mXmppConnectionService, quietHours ? "quiet_hours" : (notify ? "messages" : "silent_messages"));
471 final NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle();
472 style.setBigContentTitle(notifications.size()
473 + " "
474 + mXmppConnectionService
475 .getString(R.string.unread_conversations));
476 final StringBuilder names = new StringBuilder();
477 Conversation conversation = null;
478 for (final ArrayList<Message> messages : notifications.values()) {
479 if (messages.size() > 0) {
480 conversation = (Conversation) messages.get(0).getConversation();
481 final String name = conversation.getName().toString();
482 SpannableString styledString;
483 if (Config.HIDE_MESSAGE_TEXT_IN_NOTIFICATION) {
484 int count = messages.size();
485 styledString = new SpannableString(name + ": " + mXmppConnectionService.getResources().getQuantityString(R.plurals.x_messages, count, count));
486 styledString.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), 0);
487 style.addLine(styledString);
488 } else {
489 styledString = new SpannableString(name + ": " + UIHelper.getMessagePreview(mXmppConnectionService, messages.get(0)).first);
490 styledString.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), 0);
491 style.addLine(styledString);
492 }
493 names.append(name);
494 names.append(", ");
495 }
496 }
497 if (names.length() >= 2) {
498 names.delete(names.length() - 2, names.length());
499 }
500 mBuilder.setContentTitle(notifications.size()
501 + " "
502 + mXmppConnectionService
503 .getString(R.string.unread_conversations));
504 mBuilder.setContentText(names.toString());
505 mBuilder.setStyle(style);
506 if (conversation != null) {
507 mBuilder.setContentIntent(createContentIntent(conversation));
508 }
509 mBuilder.setGroupSummary(true);
510 mBuilder.setGroup(CONVERSATIONS_GROUP);
511 mBuilder.setDeleteIntent(createDeleteIntent(null));
512 mBuilder.setSmallIcon(R.drawable.ic_notification);
513 return mBuilder;
514 }
515
516 private Builder buildSingleConversations(final ArrayList<Message> messages, final boolean notify, final boolean quietHours) {
517 final Builder mBuilder = new NotificationCompat.Builder(mXmppConnectionService, quietHours ? "quiet_hours" : (notify ? "messages" : "silent_messages"));
518 if (messages.size() >= 1) {
519 final Conversation conversation = (Conversation) messages.get(0).getConversation();
520 mBuilder.setLargeIcon(mXmppConnectionService.getAvatarService()
521 .get(conversation, AvatarService.getSystemUiAvatarSize(mXmppConnectionService)));
522 mBuilder.setContentTitle(conversation.getName());
523 if (Config.HIDE_MESSAGE_TEXT_IN_NOTIFICATION) {
524 int count = messages.size();
525 mBuilder.setContentText(mXmppConnectionService.getResources().getQuantityString(R.plurals.x_messages, count, count));
526 } else {
527 Message message;
528 //TODO starting with Android 9 we might want to put images in MessageStyle
529 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P && (message = getImage(messages)) != null) {
530 modifyForImage(mBuilder, message, messages);
531 } else {
532 modifyForTextOnly(mBuilder, messages);
533 }
534 RemoteInput remoteInput = new RemoteInput.Builder("text_reply").setLabel(UIHelper.getMessageHint(mXmppConnectionService, conversation)).build();
535 PendingIntent markAsReadPendingIntent = createReadPendingIntent(conversation);
536 NotificationCompat.Action markReadAction = new NotificationCompat.Action.Builder(
537 R.drawable.ic_drafts_white_24dp,
538 mXmppConnectionService.getString(R.string.mark_as_read),
539 markAsReadPendingIntent)
540 .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_MARK_AS_READ)
541 .setShowsUserInterface(false)
542 .build();
543 String replyLabel = mXmppConnectionService.getString(R.string.reply);
544 NotificationCompat.Action replyAction = new NotificationCompat.Action.Builder(
545 R.drawable.ic_send_text_offline,
546 replyLabel,
547 createReplyIntent(conversation, false))
548 .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY)
549 .setShowsUserInterface(false)
550 .addRemoteInput(remoteInput).build();
551 NotificationCompat.Action wearReplyAction = new NotificationCompat.Action.Builder(R.drawable.ic_wear_reply,
552 replyLabel,
553 createReplyIntent(conversation, true)).addRemoteInput(remoteInput).build();
554 mBuilder.extend(new NotificationCompat.WearableExtender().addAction(wearReplyAction));
555 int addedActionsCount = 1;
556 mBuilder.addAction(markReadAction);
557 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
558 mBuilder.addAction(replyAction);
559 ++addedActionsCount;
560 }
561
562 if (displaySnoozeAction(messages)) {
563 String label = mXmppConnectionService.getString(R.string.snooze);
564 PendingIntent pendingSnoozeIntent = createSnoozeIntent(conversation);
565 NotificationCompat.Action snoozeAction = new NotificationCompat.Action.Builder(
566 R.drawable.ic_notifications_paused_white_24dp,
567 label,
568 pendingSnoozeIntent).build();
569 mBuilder.addAction(snoozeAction);
570 ++addedActionsCount;
571 }
572 if (addedActionsCount < 3) {
573 final Message firstLocationMessage = getFirstLocationMessage(messages);
574 if (firstLocationMessage != null) {
575 String label = mXmppConnectionService.getResources().getString(R.string.show_location);
576 PendingIntent pendingShowLocationIntent = createShowLocationIntent(firstLocationMessage);
577 NotificationCompat.Action locationAction = new NotificationCompat.Action.Builder(
578 R.drawable.ic_room_white_24dp,
579 label,
580 pendingShowLocationIntent).build();
581 mBuilder.addAction(locationAction);
582 ++addedActionsCount;
583 }
584 }
585 if (addedActionsCount < 3) {
586 Message firstDownloadableMessage = getFirstDownloadableMessage(messages);
587 if (firstDownloadableMessage != null) {
588 String label = mXmppConnectionService.getResources().getString(R.string.download_x_file, UIHelper.getFileDescriptionString(mXmppConnectionService, firstDownloadableMessage));
589 PendingIntent pendingDownloadIntent = createDownloadIntent(firstDownloadableMessage);
590 NotificationCompat.Action downloadAction = new NotificationCompat.Action.Builder(
591 R.drawable.ic_file_download_white_24dp,
592 label,
593 pendingDownloadIntent).build();
594 mBuilder.addAction(downloadAction);
595 ++addedActionsCount;
596 }
597 }
598 }
599 if (conversation.getMode() == Conversation.MODE_SINGLE) {
600 Contact contact = conversation.getContact();
601 Uri systemAccount = contact.getSystemAccount();
602 if (systemAccount != null) {
603 mBuilder.addPerson(systemAccount.toString());
604 }
605 }
606 mBuilder.setWhen(conversation.getLatestMessage().getTimeSent());
607 mBuilder.setSmallIcon(R.drawable.ic_notification);
608 mBuilder.setDeleteIntent(createDeleteIntent(conversation));
609 mBuilder.setContentIntent(createContentIntent(conversation));
610 }
611 return mBuilder;
612 }
613
614 private void modifyForImage(final Builder builder, final Message message, final ArrayList<Message> messages) {
615 try {
616 final Bitmap bitmap = mXmppConnectionService.getFileBackend().getThumbnail(message, getPixel(288), false);
617 final ArrayList<Message> tmp = new ArrayList<>();
618 for (final Message msg : messages) {
619 if (msg.getType() == Message.TYPE_TEXT
620 && msg.getTransferable() == null) {
621 tmp.add(msg);
622 }
623 }
624 final BigPictureStyle bigPictureStyle = new NotificationCompat.BigPictureStyle();
625 bigPictureStyle.bigPicture(bitmap);
626 if (tmp.size() > 0) {
627 CharSequence text = getMergedBodies(tmp);
628 bigPictureStyle.setSummaryText(text);
629 builder.setContentText(text);
630 } else {
631 builder.setContentText(UIHelper.getFileDescriptionString(mXmppConnectionService, message));
632 }
633 builder.setStyle(bigPictureStyle);
634 } catch (final IOException e) {
635 modifyForTextOnly(builder, messages);
636 }
637 }
638
639 private Person getPerson(Message message) {
640 final Contact contact = message.getContact();
641 final Person.Builder builder = new Person.Builder();
642 if (contact != null) {
643 builder.setName(contact.getDisplayName());
644 final Uri uri = contact.getSystemAccount();
645 if (uri != null) {
646 builder.setUri(uri.toString());
647 }
648 } else {
649 builder.setName(UIHelper.getMessageDisplayName(message));
650 }
651 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
652 builder.setIcon(IconCompat.createWithBitmap(mXmppConnectionService.getAvatarService().get(message, AvatarService.getSystemUiAvatarSize(mXmppConnectionService), false)));
653 }
654 return builder.build();
655 }
656
657 private void modifyForTextOnly(final Builder builder, final ArrayList<Message> messages) {
658 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
659 final Conversation conversation = (Conversation) messages.get(0).getConversation();
660 final Person.Builder meBuilder = new Person.Builder().setName(mXmppConnectionService.getString(R.string.me));
661 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
662 meBuilder.setIcon(IconCompat.createWithBitmap(mXmppConnectionService.getAvatarService().get(conversation.getAccount(), AvatarService.getSystemUiAvatarSize(mXmppConnectionService))));
663 }
664 final Person me = meBuilder.build();
665 NotificationCompat.MessagingStyle messagingStyle = new NotificationCompat.MessagingStyle(me);
666 final boolean multiple = conversation.getMode() == Conversation.MODE_MULTI;
667 if (multiple) {
668 messagingStyle.setConversationTitle(conversation.getName());
669 }
670 for (Message message : messages) {
671 final Person sender = message.getStatus() == Message.STATUS_RECEIVED ? getPerson(message) : null;
672 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && isImageMessage(message)) {
673 final Uri dataUri = FileBackend.getMediaUri(mXmppConnectionService,mXmppConnectionService.getFileBackend().getFile(message));
674 NotificationCompat.MessagingStyle.Message imageMessage = new NotificationCompat.MessagingStyle.Message(UIHelper.getMessagePreview(mXmppConnectionService, message).first, message.getTimeSent(), sender);
675 if (dataUri != null) {
676 imageMessage.setData(message.getMimeType(), dataUri);
677 }
678 messagingStyle.addMessage(imageMessage);
679 } else {
680 messagingStyle.addMessage(UIHelper.getMessagePreview(mXmppConnectionService, message).first, message.getTimeSent(), sender);
681 }
682 }
683 messagingStyle.setGroupConversation(multiple);
684 builder.setStyle(messagingStyle);
685 } else {
686 if (messages.get(0).getConversation().getMode() == Conversation.MODE_SINGLE) {
687 builder.setStyle(new NotificationCompat.BigTextStyle().bigText(getMergedBodies(messages)));
688 builder.setContentText(UIHelper.getMessagePreview(mXmppConnectionService, messages.get(messages.size()-1)).first);
689 builder.setNumber(messages.size());
690 } else {
691 final NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle();
692 SpannableString styledString;
693 for (Message message : messages) {
694 final String name = UIHelper.getMessageDisplayName(message);
695 styledString = new SpannableString(name + ": " + message.getBody());
696 styledString.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), 0);
697 style.addLine(styledString);
698 }
699 builder.setStyle(style);
700 int count = messages.size();
701 if (count == 1) {
702 final String name = UIHelper.getMessageDisplayName(messages.get(0));
703 styledString = new SpannableString(name + ": " + messages.get(0).getBody());
704 styledString.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), 0);
705 builder.setContentText(styledString);
706 } else {
707 builder.setContentText(mXmppConnectionService.getResources().getQuantityString(R.plurals.x_messages, count, count));
708 }
709 }
710 }
711 }
712
713 private Message getImage(final Iterable<Message> messages) {
714 Message image = null;
715 for (final Message message : messages) {
716 if (message.getStatus() != Message.STATUS_RECEIVED) {
717 return null;
718 }
719 if (isImageMessage(message)) {
720 image = message;
721 }
722 }
723 return image;
724 }
725
726 private static boolean isImageMessage(Message message) {
727 return message.getType() != Message.TYPE_TEXT
728 && message.getTransferable() == null
729 && !message.isDeleted()
730 && message.getEncryption() != Message.ENCRYPTION_PGP
731 && message.getFileParams().height > 0;
732 }
733
734 private Message getFirstDownloadableMessage(final Iterable<Message> messages) {
735 for (final Message message : messages) {
736 if (message.getTransferable() != null || (message.getType() == Message.TYPE_TEXT && message.treatAsDownloadable())) {
737 return message;
738 }
739 }
740 return null;
741 }
742
743 private Message getFirstLocationMessage(final Iterable<Message> messages) {
744 for (final Message message : messages) {
745 if (message.isGeoUri()) {
746 return message;
747 }
748 }
749 return null;
750 }
751
752 private CharSequence getMergedBodies(final ArrayList<Message> messages) {
753 final StringBuilder text = new StringBuilder();
754 for (Message message : messages) {
755 if (text.length() != 0) {
756 text.append("\n");
757 }
758 text.append(UIHelper.getMessagePreview(mXmppConnectionService, message).first);
759 }
760 return text.toString();
761 }
762
763 private PendingIntent createShowLocationIntent(final Message message) {
764 Iterable<Intent> intents = GeoHelper.createGeoIntentsFromMessage(mXmppConnectionService, message);
765 for (Intent intent : intents) {
766 if (intent.resolveActivity(mXmppConnectionService.getPackageManager()) != null) {
767 return PendingIntent.getActivity(mXmppConnectionService, generateRequestCode(message.getConversation(), 18), intent, PendingIntent.FLAG_UPDATE_CURRENT);
768 }
769 }
770 return createOpenConversationsIntent();
771 }
772
773 private PendingIntent createContentIntent(final String conversationUuid, final String downloadMessageUuid) {
774 final Intent viewConversationIntent = new Intent(mXmppConnectionService, ConversationsActivity.class);
775 viewConversationIntent.setAction(ConversationsActivity.ACTION_VIEW_CONVERSATION);
776 viewConversationIntent.putExtra(ConversationsActivity.EXTRA_CONVERSATION, conversationUuid);
777 if (downloadMessageUuid != null) {
778 viewConversationIntent.putExtra(ConversationsActivity.EXTRA_DOWNLOAD_UUID, downloadMessageUuid);
779 return PendingIntent.getActivity(mXmppConnectionService,
780 generateRequestCode(conversationUuid, 8),
781 viewConversationIntent,
782 PendingIntent.FLAG_UPDATE_CURRENT);
783 } else {
784 return PendingIntent.getActivity(mXmppConnectionService,
785 generateRequestCode(conversationUuid, 10),
786 viewConversationIntent,
787 PendingIntent.FLAG_UPDATE_CURRENT);
788 }
789 }
790
791 private int generateRequestCode(String uuid, int actionId) {
792 return (actionId * NOTIFICATION_ID_MULTIPLIER) + (uuid.hashCode() % NOTIFICATION_ID_MULTIPLIER);
793 }
794
795 private int generateRequestCode(Conversational conversation, int actionId) {
796 return generateRequestCode(conversation.getUuid(), actionId);
797 }
798
799 private PendingIntent createDownloadIntent(final Message message) {
800 return createContentIntent(message.getConversationUuid(), message.getUuid());
801 }
802
803 private PendingIntent createContentIntent(final Conversational conversation) {
804 return createContentIntent(conversation.getUuid(), null);
805 }
806
807 private PendingIntent createDeleteIntent(Conversation conversation) {
808 final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
809 intent.setAction(XmppConnectionService.ACTION_CLEAR_NOTIFICATION);
810 if (conversation != null) {
811 intent.putExtra("uuid", conversation.getUuid());
812 return PendingIntent.getService(mXmppConnectionService, generateRequestCode(conversation, 20), intent, 0);
813 }
814 return PendingIntent.getService(mXmppConnectionService, 0, intent, 0);
815 }
816
817 private PendingIntent createReplyIntent(Conversation conversation, boolean dismissAfterReply) {
818 final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
819 intent.setAction(XmppConnectionService.ACTION_REPLY_TO_CONVERSATION);
820 intent.putExtra("uuid", conversation.getUuid());
821 intent.putExtra("dismiss_notification", dismissAfterReply);
822 final int id = generateRequestCode(conversation, dismissAfterReply ? 12 : 14);
823 return PendingIntent.getService(mXmppConnectionService, id, intent, 0);
824 }
825
826 private PendingIntent createReadPendingIntent(Conversation conversation) {
827 final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
828 intent.setAction(XmppConnectionService.ACTION_MARK_AS_READ);
829 intent.putExtra("uuid", conversation.getUuid());
830 intent.setPackage(mXmppConnectionService.getPackageName());
831 return PendingIntent.getService(mXmppConnectionService, generateRequestCode(conversation, 16), intent, PendingIntent.FLAG_UPDATE_CURRENT);
832 }
833
834 private PendingIntent createSnoozeIntent(Conversation conversation) {
835 final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
836 intent.setAction(XmppConnectionService.ACTION_SNOOZE);
837 intent.putExtra("uuid", conversation.getUuid());
838 intent.setPackage(mXmppConnectionService.getPackageName());
839 return PendingIntent.getService(mXmppConnectionService, generateRequestCode(conversation, 22), intent, PendingIntent.FLAG_UPDATE_CURRENT);
840 }
841
842 private PendingIntent createTryAgainIntent() {
843 final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
844 intent.setAction(XmppConnectionService.ACTION_TRY_AGAIN);
845 return PendingIntent.getService(mXmppConnectionService, 45, intent, 0);
846 }
847
848 private PendingIntent createDismissErrorIntent() {
849 final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
850 intent.setAction(XmppConnectionService.ACTION_DISMISS_ERROR_NOTIFICATIONS);
851 return PendingIntent.getService(mXmppConnectionService, 69, intent, 0);
852 }
853
854 private boolean wasHighlightedOrPrivate(final Message message) {
855 if (message.getConversation() instanceof Conversation) {
856 Conversation conversation = (Conversation) message.getConversation();
857 final String nick = conversation.getMucOptions().getActualNick();
858 final Pattern highlight = generateNickHighlightPattern(nick);
859 if (message.getBody() == null || nick == null) {
860 return false;
861 }
862 final Matcher m = highlight.matcher(message.getBody());
863 return (m.find() || message.isPrivateMessage());
864 } else {
865 return false;
866 }
867 }
868
869 public void setOpenConversation(final Conversation conversation) {
870 this.mOpenConversation = conversation;
871 }
872
873 public void setIsInForeground(final boolean foreground) {
874 this.mIsInForeground = foreground;
875 }
876
877 private int getPixel(final int dp) {
878 final DisplayMetrics metrics = mXmppConnectionService.getResources()
879 .getDisplayMetrics();
880 return ((int) (dp * metrics.density));
881 }
882
883 private void markLastNotification() {
884 this.mLastNotification = SystemClock.elapsedRealtime();
885 }
886
887 private boolean inMiniGracePeriod(final Account account) {
888 final int miniGrace = account.getStatus() == Account.State.ONLINE ? Config.MINI_GRACE_PERIOD
889 : Config.MINI_GRACE_PERIOD * 2;
890 return SystemClock.elapsedRealtime() < (this.mLastNotification + miniGrace);
891 }
892
893 Notification createForegroundNotification() {
894 final Notification.Builder mBuilder = new Notification.Builder(mXmppConnectionService);
895 mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.app_name));
896 final List<Account> accounts = mXmppConnectionService.getAccounts();
897 int enabled = 0;
898 int connected = 0;
899 if (accounts != null) {
900 for (Account account : accounts) {
901 if (account.isOnlineAndConnected()) {
902 connected++;
903 enabled++;
904 } else if (account.isEnabled()) {
905 enabled++;
906 }
907 }
908 }
909 mBuilder.setContentText(mXmppConnectionService.getString(R.string.connected_accounts, connected, enabled));
910 mBuilder.setContentIntent(createOpenConversationsIntent());
911 mBuilder.setWhen(0);
912 mBuilder.setPriority(Notification.PRIORITY_MIN);
913 mBuilder.setSmallIcon(connected > 0 ? R.drawable.ic_link_white_24dp : R.drawable.ic_link_off_white_24dp);
914
915 if (Compatibility.runsTwentySix()) {
916 mBuilder.setChannelId("foreground");
917 }
918
919
920 return mBuilder.build();
921 }
922
923 private PendingIntent createOpenConversationsIntent() {
924 return PendingIntent.getActivity(mXmppConnectionService, 0, new Intent(mXmppConnectionService, ConversationsActivity.class), 0);
925 }
926
927 void updateErrorNotification() {
928 if (Config.SUPPRESS_ERROR_NOTIFICATION) {
929 cancel(ERROR_NOTIFICATION_ID);
930 return;
931 }
932 final boolean showAllErrors = QuickConversationsService.isConversations();
933 final List<Account> errors = new ArrayList<>();
934 for (final Account account : mXmppConnectionService.getAccounts()) {
935 if (account.hasErrorStatus() && account.showErrorNotification() && (showAllErrors || account.getLastErrorStatus() == Account.State.UNAUTHORIZED)) {
936 errors.add(account);
937 }
938 }
939 if (mXmppConnectionService.foregroundNotificationNeedsUpdatingWhenErrorStateChanges()) {
940 notify(FOREGROUND_NOTIFICATION_ID, createForegroundNotification());
941 }
942 final Notification.Builder mBuilder = new Notification.Builder(mXmppConnectionService);
943 if (errors.size() == 0) {
944 cancel(ERROR_NOTIFICATION_ID);
945 return;
946 } else if (errors.size() == 1) {
947 mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.problem_connecting_to_account));
948 mBuilder.setContentText(errors.get(0).getJid().asBareJid().toString());
949 } else {
950 mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.problem_connecting_to_accounts));
951 mBuilder.setContentText(mXmppConnectionService.getString(R.string.touch_to_fix));
952 }
953 mBuilder.addAction(R.drawable.ic_autorenew_white_24dp,
954 mXmppConnectionService.getString(R.string.try_again),
955 createTryAgainIntent());
956 mBuilder.setDeleteIntent(createDismissErrorIntent());
957 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
958 mBuilder.setVisibility(NotificationCompat.VISIBILITY_PRIVATE);
959 mBuilder.setSmallIcon(R.drawable.ic_warning_white_24dp);
960 } else {
961 mBuilder.setSmallIcon(R.drawable.ic_stat_alert_warning);
962 }
963 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) {
964 mBuilder.setLocalOnly(true);
965 }
966 mBuilder.setPriority(Notification.PRIORITY_LOW);
967 final Intent intent;
968 if (AccountUtils.MANAGE_ACCOUNT_ACTIVITY != null) {
969 intent = new Intent(mXmppConnectionService, AccountUtils.MANAGE_ACCOUNT_ACTIVITY);
970 } else {
971 intent = new Intent(mXmppConnectionService, EditAccountActivity.class);
972 intent.putExtra("jid", errors.get(0).getJid().asBareJid().toEscapedString());
973 intent.putExtra(EditAccountActivity.EXTRA_OPENED_FROM_NOTIFICATION, true);
974 }
975 mBuilder.setContentIntent(PendingIntent.getActivity(mXmppConnectionService, 145, intent, PendingIntent.FLAG_UPDATE_CURRENT));
976 if (Compatibility.runsTwentySix()) {
977 mBuilder.setChannelId("error");
978 }
979 notify(ERROR_NOTIFICATION_ID, mBuilder.build());
980 }
981
982 void updateFileAddingNotification(int current, Message message) {
983 Notification.Builder mBuilder = new Notification.Builder(mXmppConnectionService);
984 mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.transcoding_video));
985 mBuilder.setProgress(100, current, false);
986 mBuilder.setSmallIcon(R.drawable.ic_hourglass_empty_white_24dp);
987 mBuilder.setContentIntent(createContentIntent(message.getConversation()));
988 mBuilder.setOngoing(true);
989 if (Compatibility.runsTwentySix()) {
990 mBuilder.setChannelId("compression");
991 }
992 Notification notification = mBuilder.build();
993 notify(FOREGROUND_NOTIFICATION_ID, notification);
994 }
995
996 void dismissForcedForegroundNotification() {
997 cancel(FOREGROUND_NOTIFICATION_ID);
998 }
999
1000 private void notify(String tag, int id, Notification notification) {
1001 final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(mXmppConnectionService);
1002 try {
1003 notificationManager.notify(tag, id, notification);
1004 } catch (RuntimeException e) {
1005 Log.d(Config.LOGTAG, "unable to make notification", e);
1006 }
1007 }
1008
1009 public void notify(int id, Notification notification) {
1010 final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(mXmppConnectionService);
1011 try {
1012 notificationManager.notify(id, notification);
1013 } catch (RuntimeException e) {
1014 Log.d(Config.LOGTAG, "unable to make notification", e);
1015 }
1016 }
1017
1018 private void cancel(int id) {
1019 final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(mXmppConnectionService);
1020 try {
1021 notificationManager.cancel(id);
1022 } catch (RuntimeException e) {
1023 Log.d(Config.LOGTAG, "unable to cancel notification", e);
1024 }
1025 }
1026
1027 private void cancel(String tag, int id) {
1028 final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(mXmppConnectionService);
1029 try {
1030 notificationManager.cancel(tag, id);
1031 } catch (RuntimeException e) {
1032 Log.d(Config.LOGTAG, "unable to cancel notification", e);
1033 }
1034 }
1035}