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