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