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