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