NotificationService.java

  1package eu.siacs.conversations.services;
  2
  3import android.app.Notification;
  4import android.app.NotificationManager;
  5import android.app.PendingIntent;
  6import android.content.Context;
  7import android.content.Intent;
  8import android.content.SharedPreferences;
  9import android.graphics.Bitmap;
 10import android.net.Uri;
 11import android.os.PowerManager;
 12import android.os.SystemClock;
 13import android.support.v4.app.NotificationCompat;
 14import android.support.v4.app.NotificationCompat.BigPictureStyle;
 15import android.support.v4.app.NotificationCompat.Builder;
 16import android.support.v4.app.TaskStackBuilder;
 17import android.text.Html;
 18import android.util.DisplayMetrics;
 19import android.util.Log;
 20
 21import java.io.FileNotFoundException;
 22import java.util.ArrayList;
 23import java.util.Calendar;
 24import java.util.LinkedHashMap;
 25import java.util.List;
 26import java.util.regex.Matcher;
 27import java.util.regex.Pattern;
 28
 29import eu.siacs.conversations.Config;
 30import eu.siacs.conversations.R;
 31import eu.siacs.conversations.entities.Account;
 32import eu.siacs.conversations.entities.Conversation;
 33import eu.siacs.conversations.entities.Downloadable;
 34import eu.siacs.conversations.entities.DownloadableFile;
 35import eu.siacs.conversations.entities.Message;
 36import eu.siacs.conversations.ui.ConversationActivity;
 37import eu.siacs.conversations.ui.ManageAccountActivity;
 38import eu.siacs.conversations.ui.TimePreference;
 39
 40public class NotificationService {
 41
 42	private XmppConnectionService mXmppConnectionService;
 43
 44	private final LinkedHashMap<String, ArrayList<Message>> notifications = new LinkedHashMap<>();
 45
 46	public static final int NOTIFICATION_ID = 0x2342;
 47	public static final int FOREGROUND_NOTIFICATION_ID = 0x8899;
 48	public static final int ERROR_NOTIFICATION_ID = 0x5678;
 49
 50	private Conversation mOpenConversation;
 51	private boolean mIsInForeground;
 52	private long mLastNotification;
 53
 54	public NotificationService(XmppConnectionService service) {
 55		this.mXmppConnectionService = service;
 56	}
 57
 58	public boolean notify(final Message message) {
 59		return (message.getStatus() == Message.STATUS_RECEIVED)
 60			&& notificationsEnabled()
 61			&& !message.getConversation().isMuted()
 62			&& (message.getConversation().getMode() == Conversation.MODE_SINGLE
 63					|| conferenceNotificationsEnabled()
 64					|| wasHighlightedOrPrivate(message)
 65				 );
 66	}
 67
 68	public boolean notificationsEnabled() {
 69		return mXmppConnectionService.getPreferences().getBoolean("show_notification", true);
 70	}
 71
 72	public boolean isQuietHours() {
 73		if (!mXmppConnectionService.getPreferences().getBoolean("enable_quiet_hours", false)) {
 74			return false;
 75		}
 76		final long startTime = mXmppConnectionService.getPreferences().getLong("quiet_hours_start", TimePreference.DEFAULT_VALUE) % Config.MILLISECONDS_IN_DAY;
 77		final long endTime = mXmppConnectionService.getPreferences().getLong("quiet_hours_end", TimePreference.DEFAULT_VALUE) % Config.MILLISECONDS_IN_DAY;
 78		final long nowTime = Calendar.getInstance().getTimeInMillis() % Config.MILLISECONDS_IN_DAY;
 79
 80		if (endTime < startTime) {
 81			return nowTime > startTime || nowTime < endTime;
 82		} else {
 83			return nowTime > startTime && nowTime < endTime;
 84		}
 85	}
 86
 87	public boolean conferenceNotificationsEnabled() {
 88		return mXmppConnectionService.getPreferences().getBoolean("always_notify_in_conference", false);
 89	}
 90
 91	public void push(final Message message) {
 92		if (!notify(message)) {
 93			return;
 94		}
 95		final PowerManager pm = (PowerManager) mXmppConnectionService
 96			.getSystemService(Context.POWER_SERVICE);
 97		final boolean isScreenOn = pm.isScreenOn();
 98
 99		if (this.mIsInForeground && isScreenOn
100				&& this.mOpenConversation == message.getConversation()) {
101			return;
102				}
103		synchronized (notifications) {
104			final String conversationUuid = message.getConversationUuid();
105			if (notifications.containsKey(conversationUuid)) {
106				notifications.get(conversationUuid).add(message);
107			} else {
108				final ArrayList<Message> mList = new ArrayList<>();
109				mList.add(message);
110				notifications.put(conversationUuid, mList);
111			}
112			final Account account = message.getConversation().getAccount();
113			updateNotification((!(this.mIsInForeground && this.mOpenConversation == null) || !isScreenOn)
114					&& !account.inGracePeriod()
115					&& !this.inMiniGracePeriod(account));
116		}
117
118	}
119
120	public void clear() {
121		synchronized (notifications) {
122			notifications.clear();
123			updateNotification(false);
124		}
125	}
126
127	public void clear(final Conversation conversation) {
128		synchronized (notifications) {
129			notifications.remove(conversation.getUuid());
130			updateNotification(false);
131		}
132	}
133
134	private void updateNotification(final boolean notify) {
135		final NotificationManager notificationManager = (NotificationManager) mXmppConnectionService
136			.getSystemService(Context.NOTIFICATION_SERVICE);
137		final SharedPreferences preferences = mXmppConnectionService.getPreferences();
138
139		final String ringtone = preferences.getString("notification_ringtone", null);
140		final boolean vibrate = preferences.getBoolean("vibrate_on_notification", true);
141
142		if (notifications.size() == 0) {
143			notificationManager.cancel(NOTIFICATION_ID);
144		} else {
145			if (notify) {
146				this.markLastNotification();
147			}
148			final Builder mBuilder;
149			if (notifications.size() == 1) {
150				mBuilder = buildSingleConversations(notify);
151			} else {
152				mBuilder = buildMultipleConversation();
153			}
154			if (notify && !isQuietHours()) {
155				if (vibrate) {
156					final int dat = 70;
157					final long[] pattern = {0, 3 * dat, dat, dat};
158					mBuilder.setVibrate(pattern);
159				}
160				if (ringtone != null) {
161					mBuilder.setSound(Uri.parse(ringtone));
162				}
163			}
164			mBuilder.setSmallIcon(R.drawable.ic_notification);
165			mBuilder.setDeleteIntent(createDeleteIntent());
166			mBuilder.setLights(0xffffffff, 2000, 4000);
167			final Notification notification = mBuilder.build();
168			notificationManager.notify(NOTIFICATION_ID, notification);
169		}
170	}
171
172	private Builder buildMultipleConversation() {
173		final Builder mBuilder = new NotificationCompat.Builder(
174				mXmppConnectionService);
175		NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle();
176		style.setBigContentTitle(notifications.size()
177				+ " "
178				+ mXmppConnectionService
179				.getString(R.string.unread_conversations));
180		final StringBuilder names = new StringBuilder();
181		Conversation conversation = null;
182		for (ArrayList<Message> messages : notifications.values()) {
183			if (messages.size() > 0) {
184				conversation = messages.get(0).getConversation();
185				String name = conversation.getName();
186				style.addLine(Html.fromHtml("<b>" + name + "</b> "
187							+ getReadableBody(messages.get(0))));
188				names.append(name);
189				names.append(", ");
190			}
191		}
192		if (names.length() >= 2) {
193			names.delete(names.length() - 2, names.length());
194		}
195		mBuilder.setContentTitle(notifications.size()
196				+ " "
197				+ mXmppConnectionService
198				.getString(R.string.unread_conversations));
199		mBuilder.setContentText(names.toString());
200		mBuilder.setStyle(style);
201		if (conversation != null) {
202			mBuilder.setContentIntent(createContentIntent(conversation
203						.getUuid()));
204		}
205		return mBuilder;
206	}
207
208	private Builder buildSingleConversations(final boolean notify) {
209		final Builder mBuilder = new NotificationCompat.Builder(
210				mXmppConnectionService);
211		final ArrayList<Message> messages = notifications.values().iterator().next();
212		if (messages.size() >= 1) {
213			final Conversation conversation = messages.get(0).getConversation();
214			mBuilder.setLargeIcon(mXmppConnectionService.getAvatarService()
215					.get(conversation, getPixel(64)));
216			mBuilder.setContentTitle(conversation.getName());
217			final Message message;
218			if ((message = getImage(messages)) != null) {
219				modifyForImage(mBuilder, message, messages, notify);
220			} else {
221				modifyForTextOnly(mBuilder, messages, notify);
222			}
223			mBuilder.setContentIntent(createContentIntent(conversation
224						.getUuid()));
225		}
226		return mBuilder;
227	}
228
229	private void modifyForImage(final Builder builder, final Message message,
230			final ArrayList<Message> messages, final boolean notify) {
231		try {
232			final Bitmap bitmap = mXmppConnectionService.getFileBackend()
233				.getThumbnail(message, getPixel(288), false);
234			final ArrayList<Message> tmp = new ArrayList<>();
235			for (final Message msg : messages) {
236				if (msg.getType() == Message.TYPE_TEXT
237						&& msg.getDownloadable() == null) {
238					tmp.add(msg);
239						}
240			}
241			final BigPictureStyle bigPictureStyle = new NotificationCompat.BigPictureStyle();
242			bigPictureStyle.bigPicture(bitmap);
243			if (tmp.size() > 0) {
244				bigPictureStyle.setSummaryText(getMergedBodies(tmp));
245				builder.setContentText(getReadableBody(tmp.get(0)));
246			} else {
247				builder.setContentText(mXmppConnectionService.getString(R.string.image_file));
248			}
249			builder.setStyle(bigPictureStyle);
250		} catch (final FileNotFoundException e) {
251			modifyForTextOnly(builder, messages, notify);
252		}
253	}
254
255	private void modifyForTextOnly(final Builder builder,
256			final ArrayList<Message> messages, final boolean notify) {
257		builder.setStyle(new NotificationCompat.BigTextStyle()
258				.bigText(getMergedBodies(messages)));
259		builder.setContentText(getReadableBody(messages.get(0)));
260		if (notify) {
261			builder.setTicker(getReadableBody(messages.get(messages.size() - 1)));
262		}
263	}
264
265	private Message getImage(final ArrayList<Message> messages) {
266		for (final Message message : messages) {
267			if (message.getType() == Message.TYPE_IMAGE
268					&& message.getDownloadable() == null
269					&& message.getEncryption() != Message.ENCRYPTION_PGP) {
270				return message;
271					}
272		}
273		return null;
274	}
275
276	private String getMergedBodies(final ArrayList<Message> messages) {
277		final StringBuilder text = new StringBuilder();
278		for (int i = 0; i < messages.size(); ++i) {
279			text.append(getReadableBody(messages.get(i)));
280			if (i != messages.size() - 1) {
281				text.append("\n");
282			}
283		}
284		return text.toString();
285	}
286
287	private String getReadableBody(final Message message) {
288		if (message.getDownloadable() != null
289				&& (message.getDownloadable().getStatus() == Downloadable.STATUS_OFFER || message
290					.getDownloadable().getStatus() == Downloadable.STATUS_OFFER_CHECK_FILESIZE)) {
291			if (message.getType() == Message.TYPE_FILE) {
292				return mXmppConnectionService.getString(R.string.file_offered_for_download);
293			} else {
294				return mXmppConnectionService.getText(
295						R.string.image_offered_for_download).toString();
296			}
297		} else if (message.getEncryption() == Message.ENCRYPTION_PGP) {
298			return mXmppConnectionService.getText(
299					R.string.encrypted_message_received).toString();
300		} else if (message.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED) {
301			return mXmppConnectionService.getText(R.string.decryption_failed)
302				.toString();
303		} else if (message.getType() == Message.TYPE_FILE) {
304			DownloadableFile file = mXmppConnectionService.getFileBackend().getFile(message);
305			return mXmppConnectionService.getString(R.string.file,file.getMimeType());
306		} else if (message.getType() == Message.TYPE_IMAGE) {
307			return mXmppConnectionService.getText(R.string.image_file)
308				.toString();
309		} else {
310			return message.getBody().trim();
311		}
312	}
313
314	private PendingIntent createContentIntent(final String conversationUuid) {
315		final TaskStackBuilder stackBuilder = TaskStackBuilder
316			.create(mXmppConnectionService);
317		stackBuilder.addParentStack(ConversationActivity.class);
318
319		final Intent viewConversationIntent = new Intent(mXmppConnectionService,
320				ConversationActivity.class);
321		viewConversationIntent.setAction(Intent.ACTION_VIEW);
322		if (conversationUuid != null) {
323			viewConversationIntent.putExtra(ConversationActivity.CONVERSATION,
324					conversationUuid);
325			viewConversationIntent.setType(ConversationActivity.VIEW_CONVERSATION);
326		}
327
328		stackBuilder.addNextIntent(viewConversationIntent);
329
330		return stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
331	}
332
333	private PendingIntent createDeleteIntent() {
334		final Intent intent = new Intent(mXmppConnectionService,
335				XmppConnectionService.class);
336		intent.setAction(XmppConnectionService.ACTION_CLEAR_NOTIFICATION);
337		return PendingIntent.getService(mXmppConnectionService, 0, intent, 0);
338	}
339
340	private PendingIntent createDisableForeground() {
341		final Intent intent = new Intent(mXmppConnectionService,
342				XmppConnectionService.class);
343		intent.setAction(XmppConnectionService.ACTION_DISABLE_FOREGROUND);
344		return PendingIntent.getService(mXmppConnectionService, 0, intent, 0);
345	}
346
347	private boolean wasHighlightedOrPrivate(final Message message) {
348		final String nick = message.getConversation().getMucOptions().getActualNick();
349		final Pattern highlight = generateNickHighlightPattern(nick);
350		if (message.getBody() == null || nick == null) {
351			return false;
352		}
353		final Matcher m = highlight.matcher(message.getBody());
354		return (m.find() || message.getType() == Message.TYPE_PRIVATE);
355	}
356
357	private static Pattern generateNickHighlightPattern(final String nick) {
358		// We expect a word boundary, i.e. space or start of string, followed by
359		// the
360		// nick (matched in case-insensitive manner), followed by optional
361		// punctuation (for example "bob: i disagree" or "how are you alice?"),
362		// followed by another word boundary.
363		return Pattern.compile("\\b" + nick + "\\p{Punct}?\\b",
364				Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE);
365	}
366
367	public void setOpenConversation(final Conversation conversation) {
368		this.mOpenConversation = conversation;
369	}
370
371	public void setIsInForeground(final boolean foreground) {
372		if (foreground != this.mIsInForeground) {
373			Log.d(Config.LOGTAG,"setIsInForeground("+Boolean.toString(foreground)+")");
374		}
375		this.mIsInForeground = foreground;
376	}
377
378	private int getPixel(final int dp) {
379		final DisplayMetrics metrics = mXmppConnectionService.getResources()
380			.getDisplayMetrics();
381		return ((int) (dp * metrics.density));
382	}
383
384	private void markLastNotification() {
385		this.mLastNotification = SystemClock.elapsedRealtime();
386	}
387
388	private boolean inMiniGracePeriod(final Account account) {
389		final int miniGrace = account.getStatus() == Account.State.ONLINE ? Config.MINI_GRACE_PERIOD
390			: Config.MINI_GRACE_PERIOD * 2;
391		return SystemClock.elapsedRealtime() < (this.mLastNotification + miniGrace);
392	}
393
394	public Notification createForegroundNotification() {
395		final NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(mXmppConnectionService);
396		mBuilder.setSmallIcon(R.drawable.ic_stat_communication_import_export);
397		mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.conversations_foreground_service));
398		mBuilder.setContentText(mXmppConnectionService.getString(R.string.touch_to_disable));
399		mBuilder.setContentIntent(createDisableForeground());
400		mBuilder.setWhen(0);
401		mBuilder.setPriority(NotificationCompat.PRIORITY_MIN);
402		return mBuilder.build();
403	}
404
405	public void updateErrorNotification() {
406		final NotificationManager mNotificationManager = (NotificationManager) mXmppConnectionService.getSystemService(Context.NOTIFICATION_SERVICE);
407		final List<Account> errors = new ArrayList<>();
408		for (final Account account : mXmppConnectionService.getAccounts()) {
409			if (account.hasErrorStatus()) {
410				errors.add(account);
411			}
412		}
413		final NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(mXmppConnectionService);
414		if (errors.size() == 0) {
415			mNotificationManager.cancel(ERROR_NOTIFICATION_ID);
416			return;
417		} else if (errors.size() == 1) {
418			mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.problem_connecting_to_account));
419			mBuilder.setContentText(errors.get(0).getJid().toBareJid().toString());
420		} else {
421			mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.problem_connecting_to_accounts));
422			mBuilder.setContentText(mXmppConnectionService.getString(R.string.touch_to_fix));
423		}
424		mBuilder.setOngoing(true);
425		mBuilder.setLights(0xffffffff, 2000, 4000);
426		mBuilder.setSmallIcon(R.drawable.ic_stat_alert_warning);
427		TaskStackBuilder stackBuilder = TaskStackBuilder.create(mXmppConnectionService);
428		stackBuilder.addParentStack(ConversationActivity.class);
429
430		final Intent manageAccountsIntent = new Intent(mXmppConnectionService,ManageAccountActivity.class);
431		stackBuilder.addNextIntent(manageAccountsIntent);
432
433		final PendingIntent resultPendingIntent = stackBuilder.getPendingIntent(0,PendingIntent.FLAG_UPDATE_CURRENT);
434
435		mBuilder.setContentIntent(resultPendingIntent);
436		mNotificationManager.notify(ERROR_NOTIFICATION_ID, mBuilder.build());
437	}
438}