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