NotificationService.java

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