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(messages.size()-1)).first);
642 builder.setNumber(messages.size());
643 } else {
644 final NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle();
645 SpannableString styledString;
646 for (Message message : messages) {
647 final String name = UIHelper.getMessageDisplayName(message);
648 styledString = new SpannableString(name + ": " + message.getBody());
649 styledString.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), 0);
650 style.addLine(styledString);
651 }
652 builder.setStyle(style);
653 int count = messages.size();
654 if (count == 1) {
655 final String name = UIHelper.getMessageDisplayName(messages.get(0));
656 styledString = new SpannableString(name + ": " + messages.get(0).getBody());
657 styledString.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), 0);
658 builder.setContentText(styledString);
659 } else {
660 builder.setContentText(mXmppConnectionService.getResources().getQuantityString(R.plurals.x_messages, count, count));
661 }
662 }
663 }
664 /** message preview for Android Auto **/
665 for (Message message : messages) {
666 Pair<CharSequence, Boolean> preview = UIHelper.getMessagePreview(mXmppConnectionService, message);
667 // only show user written text
668 if (!preview.second) {
669 uBuilder.addMessage(preview.first.toString());
670 uBuilder.setLatestTimestamp(message.getTimeSent());
671 }
672 }
673 }
674
675 private Message getImage(final Iterable<Message> messages) {
676 Message image = null;
677 for (final Message message : messages) {
678 if (message.getStatus() != Message.STATUS_RECEIVED) {
679 return null;
680 }
681 if (message.getType() != Message.TYPE_TEXT
682 && message.getTransferable() == null
683 && message.getEncryption() != Message.ENCRYPTION_PGP
684 && message.getFileParams().height > 0) {
685 image = message;
686 }
687 }
688 return image;
689 }
690
691 private Message getFirstDownloadableMessage(final Iterable<Message> messages) {
692 for (final Message message : messages) {
693 if (message.getTransferable() != null || (message.getType() == Message.TYPE_TEXT && message.treatAsDownloadable())) {
694 return message;
695 }
696 }
697 return null;
698 }
699
700 private Message getFirstLocationMessage(final Iterable<Message> messages) {
701 for (final Message message : messages) {
702 if (message.isGeoUri()) {
703 return message;
704 }
705 }
706 return null;
707 }
708
709 private CharSequence getMergedBodies(final ArrayList<Message> messages) {
710 final StringBuilder text = new StringBuilder();
711 for (Message message : messages) {
712 if (text.length() != 0) {
713 text.append("\n");
714 }
715 text.append(UIHelper.getMessagePreview(mXmppConnectionService, message).first);
716 }
717 return text.toString();
718 }
719
720 private PendingIntent createShowLocationIntent(final Message message) {
721 Iterable<Intent> intents = GeoHelper.createGeoIntentsFromMessage(mXmppConnectionService, message);
722 for (Intent intent : intents) {
723 if (intent.resolveActivity(mXmppConnectionService.getPackageManager()) != null) {
724 return PendingIntent.getActivity(mXmppConnectionService, generateRequestCode(message.getConversation(), 18), intent, PendingIntent.FLAG_UPDATE_CURRENT);
725 }
726 }
727 return createOpenConversationsIntent();
728 }
729
730 private PendingIntent createContentIntent(final String conversationUuid, final String downloadMessageUuid) {
731 final Intent viewConversationIntent = new Intent(mXmppConnectionService, ConversationsActivity.class);
732 viewConversationIntent.setAction(ConversationsActivity.ACTION_VIEW_CONVERSATION);
733 viewConversationIntent.putExtra(ConversationsActivity.EXTRA_CONVERSATION, conversationUuid);
734 if (downloadMessageUuid != null) {
735 viewConversationIntent.putExtra(ConversationsActivity.EXTRA_DOWNLOAD_UUID, downloadMessageUuid);
736 return PendingIntent.getActivity(mXmppConnectionService,
737 generateRequestCode(conversationUuid, 8),
738 viewConversationIntent,
739 PendingIntent.FLAG_UPDATE_CURRENT);
740 } else {
741 return PendingIntent.getActivity(mXmppConnectionService,
742 generateRequestCode(conversationUuid, 10),
743 viewConversationIntent,
744 PendingIntent.FLAG_UPDATE_CURRENT);
745 }
746 }
747
748 private int generateRequestCode(String uuid, int actionId) {
749 return (actionId * NOTIFICATION_ID_MULTIPLIER) + (uuid.hashCode() % NOTIFICATION_ID_MULTIPLIER);
750 }
751
752 private int generateRequestCode(Conversational conversation, int actionId) {
753 return generateRequestCode(conversation.getUuid(), actionId);
754 }
755
756 private PendingIntent createDownloadIntent(final Message message) {
757 return createContentIntent(message.getConversationUuid(), message.getUuid());
758 }
759
760 private PendingIntent createContentIntent(final Conversational conversation) {
761 return createContentIntent(conversation.getUuid(), null);
762 }
763
764 private PendingIntent createDeleteIntent(Conversation conversation) {
765 final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
766 intent.setAction(XmppConnectionService.ACTION_CLEAR_NOTIFICATION);
767 if (conversation != null) {
768 intent.putExtra("uuid", conversation.getUuid());
769 return PendingIntent.getService(mXmppConnectionService, generateRequestCode(conversation, 20), intent, 0);
770 }
771 return PendingIntent.getService(mXmppConnectionService, 0, intent, 0);
772 }
773
774 private PendingIntent createReplyIntent(Conversation conversation, boolean dismissAfterReply) {
775 final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
776 intent.setAction(XmppConnectionService.ACTION_REPLY_TO_CONVERSATION);
777 intent.putExtra("uuid", conversation.getUuid());
778 intent.putExtra("dismiss_notification", dismissAfterReply);
779 final int id = generateRequestCode(conversation, dismissAfterReply ? 12 : 14);
780 return PendingIntent.getService(mXmppConnectionService, id, intent, 0);
781 }
782
783 private PendingIntent createReadPendingIntent(Conversation conversation) {
784 final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
785 intent.setAction(XmppConnectionService.ACTION_MARK_AS_READ);
786 intent.putExtra("uuid", conversation.getUuid());
787 intent.setPackage(mXmppConnectionService.getPackageName());
788 return PendingIntent.getService(mXmppConnectionService, generateRequestCode(conversation, 16), intent, PendingIntent.FLAG_UPDATE_CURRENT);
789 }
790
791 private PendingIntent createSnoozeIntent(Conversation conversation) {
792 final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
793 intent.setAction(XmppConnectionService.ACTION_SNOOZE);
794 intent.putExtra("uuid", conversation.getUuid());
795 intent.setPackage(mXmppConnectionService.getPackageName());
796 return PendingIntent.getService(mXmppConnectionService, generateRequestCode(conversation, 22), intent, PendingIntent.FLAG_UPDATE_CURRENT);
797 }
798
799 private PendingIntent createTryAgainIntent() {
800 final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
801 intent.setAction(XmppConnectionService.ACTION_TRY_AGAIN);
802 return PendingIntent.getService(mXmppConnectionService, 45, intent, 0);
803 }
804
805 private PendingIntent createDismissErrorIntent() {
806 final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
807 intent.setAction(XmppConnectionService.ACTION_DISMISS_ERROR_NOTIFICATIONS);
808 return PendingIntent.getService(mXmppConnectionService, 69, intent, 0);
809 }
810
811 private boolean wasHighlightedOrPrivate(final Message message) {
812 if (message.getConversation() instanceof Conversation) {
813 Conversation conversation = (Conversation) message.getConversation();
814 final String nick = conversation.getMucOptions().getActualNick();
815 final Pattern highlight = generateNickHighlightPattern(nick);
816 if (message.getBody() == null || nick == null) {
817 return false;
818 }
819 final Matcher m = highlight.matcher(message.getBody());
820 return (m.find() || message.getType() == Message.TYPE_PRIVATE);
821 } else {
822 return false;
823 }
824 }
825
826 public void setOpenConversation(final Conversation conversation) {
827 this.mOpenConversation = conversation;
828 }
829
830 public void setIsInForeground(final boolean foreground) {
831 this.mIsInForeground = foreground;
832 }
833
834 private int getPixel(final int dp) {
835 final DisplayMetrics metrics = mXmppConnectionService.getResources()
836 .getDisplayMetrics();
837 return ((int) (dp * metrics.density));
838 }
839
840 private void markLastNotification() {
841 this.mLastNotification = SystemClock.elapsedRealtime();
842 }
843
844 private boolean inMiniGracePeriod(final Account account) {
845 final int miniGrace = account.getStatus() == Account.State.ONLINE ? Config.MINI_GRACE_PERIOD
846 : Config.MINI_GRACE_PERIOD * 2;
847 return SystemClock.elapsedRealtime() < (this.mLastNotification + miniGrace);
848 }
849
850 Notification createForegroundNotification() {
851 final Notification.Builder mBuilder = new Notification.Builder(mXmppConnectionService);
852 mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.app_name));
853 if (Compatibility.runsAndTargetsTwentySix(mXmppConnectionService) || Config.SHOW_CONNECTED_ACCOUNTS) {
854 List<Account> accounts = mXmppConnectionService.getAccounts();
855 int enabled = 0;
856 int connected = 0;
857 for (Account account : accounts) {
858 if (account.isOnlineAndConnected()) {
859 connected++;
860 enabled++;
861 } else if (account.isEnabled()) {
862 enabled++;
863 }
864 }
865 mBuilder.setContentText(mXmppConnectionService.getString(R.string.connected_accounts, connected, enabled));
866 } else {
867 mBuilder.setContentText(mXmppConnectionService.getString(R.string.touch_to_open_conversations));
868 }
869 mBuilder.setContentIntent(createOpenConversationsIntent());
870 mBuilder.setWhen(0);
871 mBuilder.setPriority(Notification.PRIORITY_MIN);
872 mBuilder.setSmallIcon(R.drawable.ic_link_white_24dp);
873
874 if (Compatibility.runsTwentySix()) {
875 mBuilder.setChannelId("foreground");
876 }
877
878
879 return mBuilder.build();
880 }
881
882 private PendingIntent createOpenConversationsIntent() {
883 return PendingIntent.getActivity(mXmppConnectionService, 0, new Intent(mXmppConnectionService, ConversationsActivity.class), 0);
884 }
885
886 void updateErrorNotification() {
887 if (Config.SUPPRESS_ERROR_NOTIFICATION) {
888 cancel(ERROR_NOTIFICATION_ID);
889 return;
890 }
891 final boolean showAllErrors = QuickConversationsService.isConversations();
892 final List<Account> errors = new ArrayList<>();
893 for (final Account account : mXmppConnectionService.getAccounts()) {
894 if (account.hasErrorStatus() && account.showErrorNotification() && (showAllErrors || account.getLastErrorStatus() == Account.State.UNAUTHORIZED)) {
895 errors.add(account);
896 }
897 }
898 if (mXmppConnectionService.foregroundNotificationNeedsUpdatingWhenErrorStateChanges()) {
899 notify(FOREGROUND_NOTIFICATION_ID, createForegroundNotification());
900 }
901 final Notification.Builder mBuilder = new Notification.Builder(mXmppConnectionService);
902 if (errors.size() == 0) {
903 cancel(ERROR_NOTIFICATION_ID);
904 return;
905 } else if (errors.size() == 1) {
906 mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.problem_connecting_to_account));
907 mBuilder.setContentText(errors.get(0).getJid().asBareJid().toString());
908 } else {
909 mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.problem_connecting_to_accounts));
910 mBuilder.setContentText(mXmppConnectionService.getString(R.string.touch_to_fix));
911 }
912 mBuilder.addAction(R.drawable.ic_autorenew_white_24dp,
913 mXmppConnectionService.getString(R.string.try_again),
914 createTryAgainIntent());
915 mBuilder.setDeleteIntent(createDismissErrorIntent());
916 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
917 mBuilder.setVisibility(NotificationCompat.VISIBILITY_PRIVATE);
918 mBuilder.setSmallIcon(R.drawable.ic_warning_white_24dp);
919 } else {
920 mBuilder.setSmallIcon(R.drawable.ic_stat_alert_warning);
921 }
922 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) {
923 mBuilder.setLocalOnly(true);
924 }
925 mBuilder.setPriority(Notification.PRIORITY_LOW);
926 final Intent intent;
927 if (AccountUtils.MANAGE_ACCOUNT_ACTIVITY != null) {
928 intent = new Intent(mXmppConnectionService, AccountUtils.MANAGE_ACCOUNT_ACTIVITY);
929 } else {
930 intent = new Intent(mXmppConnectionService, EditAccountActivity.class);
931 intent.putExtra("jid", errors.get(0).getJid().asBareJid().toEscapedString());
932 intent.putExtra(EditAccountActivity.EXTRA_OPENED_FROM_NOTIFICATION, true);
933 }
934 mBuilder.setContentIntent(PendingIntent.getActivity(mXmppConnectionService, 145, intent, PendingIntent.FLAG_UPDATE_CURRENT));
935 if (Compatibility.runsTwentySix()) {
936 mBuilder.setChannelId("error");
937 }
938 notify(ERROR_NOTIFICATION_ID, mBuilder.build());
939 }
940
941 void updateFileAddingNotification(int current, Message message) {
942 Notification.Builder mBuilder = new Notification.Builder(mXmppConnectionService);
943 mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.transcoding_video));
944 mBuilder.setProgress(100, current, false);
945 mBuilder.setSmallIcon(R.drawable.ic_hourglass_empty_white_24dp);
946 mBuilder.setContentIntent(createContentIntent(message.getConversation()));
947 mBuilder.setOngoing(true);
948 if (Compatibility.runsTwentySix()) {
949 mBuilder.setChannelId("compression");
950 }
951 Notification notification = mBuilder.build();
952 notify(FOREGROUND_NOTIFICATION_ID, notification);
953 }
954
955 void dismissForcedForegroundNotification() {
956 cancel(FOREGROUND_NOTIFICATION_ID);
957 }
958
959 private void notify(String tag, int id, Notification notification) {
960 final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(mXmppConnectionService);
961 try {
962 notificationManager.notify(tag, id, notification);
963 } catch (RuntimeException e) {
964 Log.d(Config.LOGTAG, "unable to make notification", e);
965 }
966 }
967
968 private void notify(int id, Notification notification) {
969 final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(mXmppConnectionService);
970 try {
971 notificationManager.notify(id, notification);
972 } catch (RuntimeException e) {
973 Log.d(Config.LOGTAG, "unable to make notification", e);
974 }
975 }
976
977 private void cancel(int id) {
978 final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(mXmppConnectionService);
979 try {
980 notificationManager.cancel(id);
981 } catch (RuntimeException e) {
982 Log.d(Config.LOGTAG, "unable to cancel notification", e);
983 }
984 }
985
986 private void cancel(String tag, int id) {
987 final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(mXmppConnectionService);
988 try {
989 notificationManager.cancel(tag, id);
990 } catch (RuntimeException e) {
991 Log.d(Config.LOGTAG, "unable to cancel notification", e);
992 }
993 }
994}