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