1package eu.siacs.conversations.services;
2
3import android.app.Notification;
4import android.app.PendingIntent;
5import android.content.Intent;
6import android.content.SharedPreferences;
7import android.graphics.Bitmap;
8import android.graphics.Typeface;
9import android.net.Uri;
10import android.os.Build;
11import android.os.SystemClock;
12import android.support.v4.app.NotificationCompat;
13import android.support.v4.app.NotificationCompat.BigPictureStyle;
14import android.support.v4.app.NotificationCompat.Builder;
15import android.support.v4.app.NotificationManagerCompat;
16import android.support.v4.app.NotificationCompat.CarExtender.UnreadConversation;
17import android.support.v4.app.RemoteInput;
18import android.support.v4.content.ContextCompat;
19import android.text.SpannableString;
20import android.text.style.StyleSpan;
21import android.util.DisplayMetrics;
22import android.util.Log;
23import android.util.Pair;
24
25import java.io.FileNotFoundException;
26import java.util.ArrayList;
27import java.util.Calendar;
28import java.util.HashMap;
29import java.util.Iterator;
30import java.util.LinkedHashMap;
31import java.util.List;
32import java.util.Map;
33import java.util.concurrent.atomic.AtomicInteger;
34import java.util.regex.Matcher;
35import java.util.regex.Pattern;
36
37import eu.siacs.conversations.Config;
38import eu.siacs.conversations.R;
39import eu.siacs.conversations.entities.Account;
40import eu.siacs.conversations.entities.Contact;
41import eu.siacs.conversations.entities.Conversation;
42import eu.siacs.conversations.entities.Message;
43import eu.siacs.conversations.ui.ConversationActivity;
44import eu.siacs.conversations.ui.ManageAccountActivity;
45import eu.siacs.conversations.ui.SettingsActivity;
46import eu.siacs.conversations.ui.TimePreference;
47import eu.siacs.conversations.utils.GeoHelper;
48import eu.siacs.conversations.utils.UIHelper;
49import eu.siacs.conversations.xmpp.XmppConnection;
50
51public class NotificationService {
52
53 private static final String CONVERSATIONS_GROUP = "eu.siacs.conversations";
54 private final XmppConnectionService mXmppConnectionService;
55
56 private final LinkedHashMap<String, ArrayList<Message>> notifications = new LinkedHashMap<>();
57
58 private static final int NOTIFICATION_ID_MULTIPLIER = 1024 * 1024;
59
60 public static final int NOTIFICATION_ID = 2 * NOTIFICATION_ID_MULTIPLIER;
61 public static final int FOREGROUND_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 4;
62 public static final int ERROR_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 6;
63
64 private Conversation mOpenConversation;
65 private boolean mIsInForeground;
66 private long mLastNotification;
67
68 private final HashMap<Conversation,AtomicInteger> mBacklogMessageCounter = new HashMap<>();
69
70 public NotificationService(final XmppConnectionService service) {
71 this.mXmppConnectionService = service;
72 }
73
74 public boolean notify(final Message message) {
75 return message.getStatus() == Message.STATUS_RECEIVED
76 && notificationsEnabled()
77 && !message.getConversation().isMuted()
78 && (message.getConversation().alwaysNotify() || wasHighlightedOrPrivate(message))
79 && (!message.getConversation().isWithStranger() || notificationsFromStrangers())
80 ;
81 }
82
83 public boolean notificationsEnabled() {
84 return mXmppConnectionService.getPreferences().getBoolean("show_notification", true);
85 }
86
87 private boolean notificationsFromStrangers() {
88 return mXmppConnectionService.getPreferences().getBoolean("notifications_from_strangers",
89 mXmppConnectionService.getResources().getBoolean(R.bool.notifications_from_strangers));
90 }
91
92 public boolean isQuietHours() {
93 if (!mXmppConnectionService.getPreferences().getBoolean("enable_quiet_hours", false)) {
94 return false;
95 }
96 final long startTime = mXmppConnectionService.getPreferences().getLong("quiet_hours_start", TimePreference.DEFAULT_VALUE) % Config.MILLISECONDS_IN_DAY;
97 final long endTime = mXmppConnectionService.getPreferences().getLong("quiet_hours_end", TimePreference.DEFAULT_VALUE) % Config.MILLISECONDS_IN_DAY;
98 final long nowTime = Calendar.getInstance().getTimeInMillis() % Config.MILLISECONDS_IN_DAY;
99
100 if (endTime < startTime) {
101 return nowTime > startTime || nowTime < endTime;
102 } else {
103 return nowTime > startTime && nowTime < endTime;
104 }
105 }
106
107 public void pushFromBacklog(final Message message) {
108 if (notify(message)) {
109 synchronized (notifications) {
110 getBacklogMessageCounter(message.getConversation()).incrementAndGet();
111 pushToStack(message);
112 }
113 }
114 }
115
116 private AtomicInteger getBacklogMessageCounter(Conversation conversation) {
117 synchronized (mBacklogMessageCounter) {
118 if (!mBacklogMessageCounter.containsKey(conversation)) {
119 mBacklogMessageCounter.put(conversation,new AtomicInteger(0));
120 }
121 return mBacklogMessageCounter.get(conversation);
122 }
123 }
124
125 public void pushFromDirectReply(final Message message) {
126 synchronized (notifications) {
127 pushToStack(message);
128 updateNotification(false);
129 }
130 }
131
132 public void finishBacklog(boolean notify, Account account) {
133 synchronized (notifications) {
134 mXmppConnectionService.updateUnreadCountBadge();
135 if (account == null || !notify) {
136 updateNotification(notify);
137 } else {
138 updateNotification(getBacklogMessageCount(account) > 0);
139 }
140 }
141 }
142
143 private int getBacklogMessageCount(Account account) {
144 int count = 0;
145 synchronized (this.mBacklogMessageCounter) {
146 for(Iterator<Map.Entry<Conversation, AtomicInteger>> it = mBacklogMessageCounter.entrySet().iterator(); it.hasNext(); ) {
147 Map.Entry<Conversation, AtomicInteger> entry = it.next();
148 if (entry.getKey().getAccount() == account) {
149 count += entry.getValue().get();
150 it.remove();
151 }
152 }
153 }
154 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": backlog message count="+count);
155 return count;
156 }
157
158 public void finishBacklog(boolean notify) {
159 finishBacklog(notify,null);
160 }
161
162 private void pushToStack(final Message message) {
163 final String conversationUuid = message.getConversationUuid();
164 if (notifications.containsKey(conversationUuid)) {
165 notifications.get(conversationUuid).add(message);
166 } else {
167 final ArrayList<Message> mList = new ArrayList<>();
168 mList.add(message);
169 notifications.put(conversationUuid, mList);
170 }
171 }
172
173 public void push(final Message message) {
174 synchronized (message.getConversation().getAccount()) {
175 final XmppConnection connection = message.getConversation().getAccount().getXmppConnection();
176 if (connection.isWaitingForSmCatchup()) {
177 connection.incrementSmCatchupMessageCounter();
178 pushFromBacklog(message);
179 } else {
180 pushNow(message);
181 }
182 }
183 }
184
185 private void pushNow(final Message message) {
186 mXmppConnectionService.updateUnreadCountBadge();
187 if (!notify(message)) {
188 Log.d(Config.LOGTAG,message.getConversation().getAccount().getJid().toBareJid()+": suppressing notification because turned off");
189 return;
190 }
191 final boolean isScreenOn = mXmppConnectionService.isInteractive();
192 if (this.mIsInForeground && isScreenOn && this.mOpenConversation == message.getConversation()) {
193 Log.d(Config.LOGTAG,message.getConversation().getAccount().getJid().toBareJid()+": suppressing notification because conversation is open");
194 return;
195 }
196 synchronized (notifications) {
197 pushToStack(message);
198 final Account account = message.getConversation().getAccount();
199 final boolean doNotify = (!(this.mIsInForeground && this.mOpenConversation == null) || !isScreenOn)
200 && !account.inGracePeriod()
201 && !this.inMiniGracePeriod(account);
202 updateNotification(doNotify);
203 }
204 }
205
206 public void clear() {
207 synchronized (notifications) {
208 for(ArrayList<Message> messages : notifications.values()) {
209 markAsReadIfHasDirectReply(messages);
210 }
211 notifications.clear();
212 updateNotification(false);
213 }
214 }
215
216 public void clear(final Conversation conversation) {
217 synchronized (this.mBacklogMessageCounter) {
218 this.mBacklogMessageCounter.remove(conversation);
219 }
220 synchronized (notifications) {
221 markAsReadIfHasDirectReply(conversation);
222 notifications.remove(conversation.getUuid());
223 final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(mXmppConnectionService);
224 notificationManager.cancel(conversation.getUuid(), NOTIFICATION_ID);
225 updateNotification(false);
226 }
227 }
228
229 private void markAsReadIfHasDirectReply(final Conversation conversation) {
230 markAsReadIfHasDirectReply(notifications.get(conversation.getUuid()));
231 }
232
233 private void markAsReadIfHasDirectReply(final ArrayList<Message> messages) {
234 if (messages != null && messages.size() > 0) {
235 Message last = messages.get(messages.size() - 1);
236 if (last.getStatus() != Message.STATUS_RECEIVED) {
237 if (mXmppConnectionService.markRead(last.getConversation(), false)) {
238 mXmppConnectionService.updateConversationUi();
239 }
240 }
241 }
242 }
243
244 private void setNotificationColor(final Builder mBuilder) {
245 mBuilder.setColor(ContextCompat.getColor(mXmppConnectionService, R.color.primary500));
246 }
247
248 public void updateNotification(final boolean notify) {
249 Log.d(Config.LOGTAG,"updateNotification("+Boolean.toString(notify)+")");
250 final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(mXmppConnectionService);
251 final SharedPreferences preferences = mXmppConnectionService.getPreferences();
252
253 if (notifications.size() == 0) {
254 notificationManager.cancel(NOTIFICATION_ID);
255 } else {
256 if (notify) {
257 this.markLastNotification();
258 }
259 final Builder mBuilder;
260 if (notifications.size() == 1 && Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
261 mBuilder = buildSingleConversations(notifications.values().iterator().next());
262 modifyForSoundVibrationAndLight(mBuilder, notify, preferences);
263 notificationManager.notify(NOTIFICATION_ID, mBuilder.build());
264 } else {
265 mBuilder = buildMultipleConversation();
266 modifyForSoundVibrationAndLight(mBuilder, notify, preferences);
267 for(Map.Entry<String,ArrayList<Message>> entry : notifications.entrySet()) {
268 Builder singleBuilder = buildSingleConversations(entry.getValue());
269 singleBuilder.setGroup(CONVERSATIONS_GROUP);
270 modifyForSoundVibrationAndLight(singleBuilder,notify,preferences);
271 notificationManager.notify(entry.getKey(), NOTIFICATION_ID ,singleBuilder.build());
272 }
273 notificationManager.notify(NOTIFICATION_ID, mBuilder.build());
274 }
275 }
276 }
277
278
279 private void modifyForSoundVibrationAndLight(Builder mBuilder, boolean notify, SharedPreferences preferences) {
280 final String ringtone = preferences.getString("notification_ringtone", null);
281 final boolean vibrate = preferences.getBoolean("vibrate_on_notification", true);
282 final boolean led = preferences.getBoolean("led", true);
283 if (notify && !isQuietHours()) {
284 if (vibrate) {
285 final int dat = 70;
286 final long[] pattern = {0, 3 * dat, dat, dat};
287 mBuilder.setVibrate(pattern);
288 } else {
289 mBuilder.setVibrate(new long[]{0});
290 }
291 if (ringtone != null) {
292 mBuilder.setSound(Uri.parse(ringtone));
293 }
294 }
295 if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
296 mBuilder.setCategory(Notification.CATEGORY_MESSAGE);
297 }
298 mBuilder.setPriority(notify ? NotificationCompat.PRIORITY_DEFAULT : NotificationCompat.PRIORITY_LOW);
299 setNotificationColor(mBuilder);
300 mBuilder.setDefaults(0);
301 if (led) {
302 mBuilder.setLights(0xff00FF00, 2000, 3000);
303 }
304 }
305
306 private Builder buildMultipleConversation() {
307 final Builder mBuilder = new NotificationCompat.Builder(
308 mXmppConnectionService);
309 final NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle();
310 style.setBigContentTitle(notifications.size()
311 + " "
312 + mXmppConnectionService
313 .getString(R.string.unread_conversations));
314 final StringBuilder names = new StringBuilder();
315 Conversation conversation = null;
316 for (final ArrayList<Message> messages : notifications.values()) {
317 if (messages.size() > 0) {
318 conversation = messages.get(0).getConversation();
319 final String name = conversation.getName();
320 SpannableString styledString;
321 if (Config.HIDE_MESSAGE_TEXT_IN_NOTIFICATION) {
322 int count = messages.size();
323 styledString = new SpannableString(name + ": " + mXmppConnectionService.getResources().getQuantityString(R.plurals.x_messages,count,count));
324 styledString.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), 0);
325 style.addLine(styledString);
326 } else {
327 styledString = new SpannableString(name + ": " + UIHelper.getMessagePreview(mXmppConnectionService, messages.get(0)).first);
328 styledString.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), 0);
329 style.addLine(styledString);
330 }
331 names.append(name);
332 names.append(", ");
333 }
334 }
335 if (names.length() >= 2) {
336 names.delete(names.length() - 2, names.length());
337 }
338 mBuilder.setContentTitle(notifications.size()
339 + " "
340 + mXmppConnectionService
341 .getString(R.string.unread_conversations));
342 mBuilder.setContentText(names.toString());
343 mBuilder.setStyle(style);
344 if (conversation != null) {
345 mBuilder.setContentIntent(createContentIntent(conversation));
346 }
347 mBuilder.setGroupSummary(true);
348 mBuilder.setGroup(CONVERSATIONS_GROUP);
349 mBuilder.setDeleteIntent(createDeleteIntent(null));
350 mBuilder.setSmallIcon(R.drawable.ic_notification);
351 return mBuilder;
352 }
353
354 private Builder buildSingleConversations(final ArrayList<Message> messages) {
355 final Builder mBuilder = new NotificationCompat.Builder(mXmppConnectionService);
356 if (messages.size() >= 1) {
357 final Conversation conversation = messages.get(0).getConversation();
358 final UnreadConversation.Builder mUnreadBuilder = new UnreadConversation.Builder(conversation.getName());
359 mBuilder.setLargeIcon(mXmppConnectionService.getAvatarService()
360 .get(conversation, getPixel(64)));
361 mBuilder.setContentTitle(conversation.getName());
362 if (Config.HIDE_MESSAGE_TEXT_IN_NOTIFICATION) {
363 int count = messages.size();
364 mBuilder.setContentText(mXmppConnectionService.getResources().getQuantityString(R.plurals.x_messages,count,count));
365 } else {
366 Message message;
367 if ((message = getImage(messages)) != null) {
368 modifyForImage(mBuilder, mUnreadBuilder, message, messages);
369 } else {
370 modifyForTextOnly(mBuilder, mUnreadBuilder, messages);
371 }
372 RemoteInput remoteInput = new RemoteInput.Builder("text_reply").setLabel(UIHelper.getMessageHint(mXmppConnectionService, conversation)).build();
373 NotificationCompat.Action replyAction = new NotificationCompat.Action.Builder(R.drawable.ic_send_text_offline, "Reply", createReplyIntent(conversation, false)).addRemoteInput(remoteInput).build();
374 NotificationCompat.Action wearReplyAction = new NotificationCompat.Action.Builder(R.drawable.ic_wear_reply, "Reply", createReplyIntent(conversation, true)).addRemoteInput(remoteInput).build();
375 mBuilder.extend(new NotificationCompat.WearableExtender().addAction(wearReplyAction));
376 mUnreadBuilder.setReplyAction(createReplyIntent(conversation, true), remoteInput);
377 mUnreadBuilder.setReadPendingIntent(createReadPendingIntent(conversation));
378 mBuilder.extend(new NotificationCompat.CarExtender().setUnreadConversation(mUnreadBuilder.build()));
379 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
380 mBuilder.addAction(replyAction);
381 }
382 if ((message = getFirstDownloadableMessage(messages)) != null) {
383 mBuilder.addAction(
384 Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ?
385 R.drawable.ic_file_download_white_24dp : R.drawable.ic_action_download,
386 mXmppConnectionService.getResources().getString(R.string.download_x_file,
387 UIHelper.getFileDescriptionString(mXmppConnectionService, message)),
388 createDownloadIntent(message)
389 );
390 }
391 if ((message = getFirstLocationMessage(messages)) != null) {
392 mBuilder.addAction(R.drawable.ic_room_white_24dp,
393 mXmppConnectionService.getString(R.string.show_location),
394 createShowLocationIntent(message));
395 }
396 }
397 if (conversation.getMode() == Conversation.MODE_SINGLE) {
398 Contact contact = conversation.getContact();
399 Uri systemAccount = contact.getSystemAccount();
400 if (systemAccount != null) {
401 mBuilder.addPerson(systemAccount.toString());
402 }
403 }
404 mBuilder.setWhen(conversation.getLatestMessage().getTimeSent());
405 mBuilder.setSmallIcon(R.drawable.ic_notification);
406 mBuilder.setDeleteIntent(createDeleteIntent(conversation));
407 mBuilder.setContentIntent(createContentIntent(conversation));
408 }
409 return mBuilder;
410 }
411
412 private void modifyForImage(final Builder builder, final UnreadConversation.Builder uBuilder,
413 final Message message, final ArrayList<Message> messages) {
414 try {
415 final Bitmap bitmap = mXmppConnectionService.getFileBackend()
416 .getThumbnail(message, getPixel(288), false);
417 final ArrayList<Message> tmp = new ArrayList<>();
418 for (final Message msg : messages) {
419 if (msg.getType() == Message.TYPE_TEXT
420 && msg.getTransferable() == null) {
421 tmp.add(msg);
422 }
423 }
424 final BigPictureStyle bigPictureStyle = new NotificationCompat.BigPictureStyle();
425 bigPictureStyle.bigPicture(bitmap);
426 if (tmp.size() > 0) {
427 CharSequence text = getMergedBodies(tmp);
428 bigPictureStyle.setSummaryText(text);
429 builder.setContentText(text);
430 } else {
431 builder.setContentText(mXmppConnectionService.getString(
432 R.string.received_x_file,
433 UIHelper.getFileDescriptionString(mXmppConnectionService, message)));
434 }
435 builder.setStyle(bigPictureStyle);
436 } catch (final FileNotFoundException e) {
437 modifyForTextOnly(builder, uBuilder, messages);
438 }
439 }
440
441 private void modifyForTextOnly(final Builder builder, final UnreadConversation.Builder uBuilder, final ArrayList<Message> messages) {
442 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
443 NotificationCompat.MessagingStyle messagingStyle = new NotificationCompat.MessagingStyle(mXmppConnectionService.getString(R.string.me));
444 Conversation conversation = messages.get(0).getConversation();
445 if (conversation.getMode() == Conversation.MODE_MULTI) {
446 messagingStyle.setConversationTitle(conversation.getName());
447 }
448 for (Message message : messages) {
449 String sender = message.getStatus() == Message.STATUS_RECEIVED ? UIHelper.getMessageDisplayName(message) : null;
450 messagingStyle.addMessage(UIHelper.getMessagePreview(mXmppConnectionService,message).first, message.getTimeSent(), sender);
451 }
452 builder.setStyle(messagingStyle);
453 } else {
454 if(messages.get(0).getConversation().getMode() == Conversation.MODE_SINGLE) {
455 builder.setStyle(new NotificationCompat.BigTextStyle().bigText(getMergedBodies(messages)));
456 builder.setContentText(UIHelper.getMessagePreview(mXmppConnectionService, messages.get(0)).first);
457 } else {
458 final NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle();
459 SpannableString styledString;
460 for (Message message : messages) {
461 final String name = UIHelper.getMessageDisplayName(message);
462 styledString = new SpannableString(name + ": " + message.getBody());
463 styledString.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), 0);
464 style.addLine(styledString);
465 }
466 builder.setStyle(style);
467 int count = messages.size();
468 if(count == 1) {
469 final String name = UIHelper.getMessageDisplayName(messages.get(0));
470 styledString = new SpannableString(name + ": " + messages.get(0).getBody());
471 styledString.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), 0);
472 builder.setContentText(styledString);
473 } else {
474 builder.setContentText(mXmppConnectionService.getResources().getQuantityString(R.plurals.x_messages,count,count));
475 }
476 }
477 }
478 /** message preview for Android Auto **/
479 for (Message message : messages) {
480 Pair<String,Boolean> preview = UIHelper.getMessagePreview(mXmppConnectionService, message);
481 // only show user written text
482 if (preview.second == false) {
483 uBuilder.addMessage(preview.first);
484 uBuilder.setLatestTimestamp(message.getTimeSent());
485 }
486 }
487 }
488
489 private Message getImage(final Iterable<Message> messages) {
490 Message image = null;
491 for (final Message message : messages) {
492 if (message.getStatus() != Message.STATUS_RECEIVED) {
493 return null;
494 }
495 if (message.getType() != Message.TYPE_TEXT
496 && message.getTransferable() == null
497 && message.getEncryption() != Message.ENCRYPTION_PGP
498 && message.getFileParams().height > 0) {
499 image = message;
500 }
501 }
502 return image;
503 }
504
505 private Message getFirstDownloadableMessage(final Iterable<Message> messages) {
506 for (final Message message : messages) {
507 if (message.getTransferable() != null || (message.getType() == Message.TYPE_TEXT && message.treatAsDownloadable())) {
508 return message;
509 }
510 }
511 return null;
512 }
513
514 private Message getFirstLocationMessage(final Iterable<Message> messages) {
515 for (final Message message : messages) {
516 if (GeoHelper.isGeoUri(message.getBody())) {
517 return message;
518 }
519 }
520 return null;
521 }
522
523 private CharSequence getMergedBodies(final ArrayList<Message> messages) {
524 final StringBuilder text = new StringBuilder();
525 for(Message message : messages) {
526 if (text.length() != 0) {
527 text.append("\n");
528 }
529 text.append(UIHelper.getMessagePreview(mXmppConnectionService, message).first);
530 }
531 return text.toString();
532 }
533
534 private PendingIntent createShowLocationIntent(final Message message) {
535 Iterable<Intent> intents = GeoHelper.createGeoIntentsFromMessage(message);
536 for (Intent intent : intents) {
537 if (intent.resolveActivity(mXmppConnectionService.getPackageManager()) != null) {
538 return PendingIntent.getActivity(mXmppConnectionService, 18, intent, PendingIntent.FLAG_UPDATE_CURRENT);
539 }
540 }
541 return createOpenConversationsIntent();
542 }
543
544 private PendingIntent createContentIntent(final String conversationUuid, final String downloadMessageUuid) {
545 final Intent viewConversationIntent = new Intent(mXmppConnectionService,ConversationActivity.class);
546 viewConversationIntent.setAction(ConversationActivity.ACTION_VIEW_CONVERSATION);
547 viewConversationIntent.putExtra(ConversationActivity.CONVERSATION, conversationUuid);
548 if (downloadMessageUuid != null) {
549 viewConversationIntent.putExtra(ConversationActivity.EXTRA_DOWNLOAD_UUID, downloadMessageUuid);
550 return PendingIntent.getActivity(mXmppConnectionService,
551 (conversationUuid.hashCode() % NOTIFICATION_ID_MULTIPLIER) + 8 * NOTIFICATION_ID_MULTIPLIER,
552 viewConversationIntent,
553 PendingIntent.FLAG_UPDATE_CURRENT);
554 } else {
555 return PendingIntent.getActivity(mXmppConnectionService,
556 (conversationUuid.hashCode() % NOTIFICATION_ID_MULTIPLIER) + 10 * NOTIFICATION_ID_MULTIPLIER,
557 viewConversationIntent,
558 PendingIntent.FLAG_UPDATE_CURRENT);
559 }
560 }
561
562 private PendingIntent createDownloadIntent(final Message message) {
563 return createContentIntent(message.getConversationUuid(), message.getUuid());
564 }
565
566 private PendingIntent createContentIntent(final Conversation conversation) {
567 return createContentIntent(conversation.getUuid(), null);
568 }
569
570 private PendingIntent createDeleteIntent(Conversation conversation) {
571 final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
572 intent.setAction(XmppConnectionService.ACTION_CLEAR_NOTIFICATION);
573 if (conversation != null) {
574 intent.putExtra("uuid", conversation.getUuid());
575 return PendingIntent.getService(mXmppConnectionService, (conversation.getUuid().hashCode() % NOTIFICATION_ID_MULTIPLIER) + 12 * NOTIFICATION_ID_MULTIPLIER, intent, 0);
576 }
577 return PendingIntent.getService(mXmppConnectionService, 0, intent, 0);
578 }
579
580 private PendingIntent createReplyIntent(Conversation conversation, boolean dismissAfterReply) {
581 final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
582 intent.setAction(XmppConnectionService.ACTION_REPLY_TO_CONVERSATION);
583 intent.putExtra("uuid",conversation.getUuid());
584 intent.putExtra("dismiss_notification",dismissAfterReply);
585 int id = (conversation.getUuid().hashCode() % NOTIFICATION_ID_MULTIPLIER) + (dismissAfterReply ? 12 : 14) * NOTIFICATION_ID_MULTIPLIER;
586 return PendingIntent.getService(mXmppConnectionService, id, intent, 0);
587 }
588
589 private PendingIntent createReadPendingIntent(Conversation conversation) {
590 final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
591 intent.setAction(XmppConnectionService.ACTION_MARK_AS_READ);
592 intent.putExtra("uuid", conversation.getUuid());
593 intent.setPackage(mXmppConnectionService.getPackageName());
594 return PendingIntent.getService(mXmppConnectionService, (conversation.getUuid().hashCode() % NOTIFICATION_ID_MULTIPLIER) + 16 * NOTIFICATION_ID_MULTIPLIER, intent, PendingIntent.FLAG_UPDATE_CURRENT);
595 }
596
597 private PendingIntent createDisableForeground() {
598 final Intent intent = new Intent(mXmppConnectionService,
599 XmppConnectionService.class);
600 intent.setAction(XmppConnectionService.ACTION_DISABLE_FOREGROUND);
601 return PendingIntent.getService(mXmppConnectionService, 34, intent, 0);
602 }
603
604 private PendingIntent createTryAgainIntent() {
605 final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
606 intent.setAction(XmppConnectionService.ACTION_TRY_AGAIN);
607 return PendingIntent.getService(mXmppConnectionService, 45, intent, 0);
608 }
609
610 private PendingIntent createDismissErrorIntent() {
611 final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
612 intent.setAction(XmppConnectionService.ACTION_DISMISS_ERROR_NOTIFICATIONS);
613 return PendingIntent.getService(mXmppConnectionService, 69, intent, 0);
614 }
615
616 private boolean wasHighlightedOrPrivate(final Message message) {
617 final String nick = message.getConversation().getMucOptions().getActualNick();
618 final Pattern highlight = generateNickHighlightPattern(nick);
619 if (message.getBody() == null || nick == null) {
620 return false;
621 }
622 final Matcher m = highlight.matcher(message.getBody());
623 return (m.find() || message.getType() == Message.TYPE_PRIVATE);
624 }
625
626 public static Pattern generateNickHighlightPattern(final String nick) {
627 // We expect a word boundary, i.e. space or start of string, followed by
628 // the
629 // nick (matched in case-insensitive manner), followed by optional
630 // punctuation (for example "bob: i disagree" or "how are you alice?"),
631 // followed by another word boundary.
632 return Pattern.compile("\\b" + Pattern.quote(nick) + "\\p{Punct}?\\b",
633 Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE);
634 }
635
636 public void setOpenConversation(final Conversation conversation) {
637 this.mOpenConversation = conversation;
638 }
639
640 public void setIsInForeground(final boolean foreground) {
641 this.mIsInForeground = foreground;
642 }
643
644 private int getPixel(final int dp) {
645 final DisplayMetrics metrics = mXmppConnectionService.getResources()
646 .getDisplayMetrics();
647 return ((int) (dp * metrics.density));
648 }
649
650 private void markLastNotification() {
651 this.mLastNotification = SystemClock.elapsedRealtime();
652 }
653
654 private boolean inMiniGracePeriod(final Account account) {
655 final int miniGrace = account.getStatus() == Account.State.ONLINE ? Config.MINI_GRACE_PERIOD
656 : Config.MINI_GRACE_PERIOD * 2;
657 return SystemClock.elapsedRealtime() < (this.mLastNotification + miniGrace);
658 }
659
660 public Notification createForegroundNotification() {
661 final NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(mXmppConnectionService);
662
663 mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.conversations_foreground_service));
664 if (Config.SHOW_CONNECTED_ACCOUNTS) {
665 List<Account> accounts = mXmppConnectionService.getAccounts();
666 int enabled = 0;
667 int connected = 0;
668 for (Account account : accounts) {
669 if (account.isOnlineAndConnected()) {
670 connected++;
671 enabled++;
672 } else if (!account.isOptionSet(Account.OPTION_DISABLED)) {
673 enabled++;
674 }
675 }
676 mBuilder.setContentText(mXmppConnectionService.getString(R.string.connected_accounts, connected, enabled));
677 } else {
678 mBuilder.setContentText(mXmppConnectionService.getString(R.string.touch_to_open_conversations));
679 }
680 mBuilder.setContentIntent(createOpenConversationsIntent());
681 mBuilder.setWhen(0);
682 mBuilder.setPriority(Config.SHOW_CONNECTED_ACCOUNTS ? NotificationCompat.PRIORITY_DEFAULT : NotificationCompat.PRIORITY_MIN);
683 mBuilder.setSmallIcon(R.drawable.ic_link_white_24dp);
684 if (Config.SHOW_DISABLE_FOREGROUND) {
685 final int cancelIcon;
686 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
687 mBuilder.setCategory(Notification.CATEGORY_SERVICE);
688 cancelIcon = R.drawable.ic_cancel_white_24dp;
689 } else {
690 cancelIcon = R.drawable.ic_action_cancel;
691 }
692 mBuilder.addAction(cancelIcon,
693 mXmppConnectionService.getString(R.string.disable_foreground_service),
694 createDisableForeground());
695 }
696 return mBuilder.build();
697 }
698
699 private PendingIntent createOpenConversationsIntent() {
700 return PendingIntent.getActivity(mXmppConnectionService, 0, new Intent(mXmppConnectionService, ConversationActivity.class), 0);
701 }
702
703 public void updateErrorNotification() {
704 final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(mXmppConnectionService);
705 final List<Account> errors = new ArrayList<>();
706 for (final Account account : mXmppConnectionService.getAccounts()) {
707 if (account.hasErrorStatus() && account.showErrorNotification()) {
708 errors.add(account);
709 }
710 }
711 if (mXmppConnectionService.getPreferences().getBoolean(SettingsActivity.KEEP_FOREGROUND_SERVICE, false)) {
712 notificationManager.notify(FOREGROUND_NOTIFICATION_ID, createForegroundNotification());
713 }
714 final NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(mXmppConnectionService);
715 if (errors.size() == 0) {
716 notificationManager.cancel(ERROR_NOTIFICATION_ID);
717 return;
718 } else if (errors.size() == 1) {
719 mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.problem_connecting_to_account));
720 mBuilder.setContentText(errors.get(0).getJid().toBareJid().toString());
721 } else {
722 mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.problem_connecting_to_accounts));
723 mBuilder.setContentText(mXmppConnectionService.getString(R.string.touch_to_fix));
724 }
725 mBuilder.addAction(R.drawable.ic_autorenew_white_24dp,
726 mXmppConnectionService.getString(R.string.try_again),
727 createTryAgainIntent());
728 mBuilder.setDeleteIntent(createDismissErrorIntent());
729 mBuilder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC);
730 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
731 mBuilder.setSmallIcon(R.drawable.ic_warning_white_24dp);
732 } else {
733 mBuilder.setSmallIcon(R.drawable.ic_stat_alert_warning);
734 }
735 mBuilder.setContentIntent(PendingIntent.getActivity(mXmppConnectionService,
736 145,
737 new Intent(mXmppConnectionService,ManageAccountActivity.class),
738 PendingIntent.FLAG_UPDATE_CURRENT));
739 notificationManager.notify(ERROR_NOTIFICATION_ID, mBuilder.build());
740 }
741}