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