1package eu.siacs.conversations.services;
2
3import android.app.Notification;
4import android.app.NotificationChannel;
5import android.app.NotificationChannelGroup;
6import android.app.NotificationManager;
7import android.app.PendingIntent;
8import android.content.Context;
9import android.content.Intent;
10import android.content.SharedPreferences;
11import android.content.res.Resources;
12import android.graphics.Bitmap;
13import android.graphics.Typeface;
14import android.media.AudioAttributes;
15import android.media.Ringtone;
16import android.media.RingtoneManager;
17import android.net.Uri;
18import android.os.Build;
19import android.os.SystemClock;
20import android.os.Vibrator;
21import android.preference.PreferenceManager;
22import android.text.SpannableString;
23import android.text.style.StyleSpan;
24import android.util.DisplayMetrics;
25import android.util.Log;
26
27import androidx.annotation.RequiresApi;
28import androidx.core.app.NotificationCompat;
29import androidx.core.app.NotificationCompat.BigPictureStyle;
30import androidx.core.app.NotificationCompat.Builder;
31import androidx.core.app.NotificationManagerCompat;
32import androidx.core.app.Person;
33import androidx.core.app.RemoteInput;
34import androidx.core.content.ContextCompat;
35import androidx.core.graphics.drawable.IconCompat;
36
37import com.google.common.base.Strings;
38import com.google.common.collect.Iterables;
39
40import java.io.File;
41import java.io.IOException;
42import java.util.ArrayList;
43import java.util.Calendar;
44import java.util.Collections;
45import java.util.HashMap;
46import java.util.Iterator;
47import java.util.LinkedHashMap;
48import java.util.List;
49import java.util.Map;
50import java.util.Set;
51import java.util.concurrent.Executors;
52import java.util.concurrent.ScheduledExecutorService;
53import java.util.concurrent.ScheduledFuture;
54import java.util.concurrent.TimeUnit;
55import java.util.concurrent.atomic.AtomicInteger;
56import java.util.regex.Matcher;
57import java.util.regex.Pattern;
58
59import eu.siacs.conversations.Config;
60import eu.siacs.conversations.R;
61import eu.siacs.conversations.entities.Account;
62import eu.siacs.conversations.entities.Contact;
63import eu.siacs.conversations.entities.Conversation;
64import eu.siacs.conversations.entities.Conversational;
65import eu.siacs.conversations.entities.Message;
66import eu.siacs.conversations.persistance.FileBackend;
67import eu.siacs.conversations.ui.ConversationsActivity;
68import eu.siacs.conversations.ui.EditAccountActivity;
69import eu.siacs.conversations.ui.RtpSessionActivity;
70import eu.siacs.conversations.ui.TimePreference;
71import eu.siacs.conversations.utils.AccountUtils;
72import eu.siacs.conversations.utils.Compatibility;
73import eu.siacs.conversations.utils.GeoHelper;
74import eu.siacs.conversations.utils.TorServiceUtils;
75import eu.siacs.conversations.utils.UIHelper;
76import eu.siacs.conversations.xmpp.XmppConnection;
77import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection;
78import eu.siacs.conversations.xmpp.jingle.Media;
79
80public class NotificationService {
81
82 private static final ScheduledExecutorService SCHEDULED_EXECUTOR_SERVICE =
83 Executors.newSingleThreadScheduledExecutor();
84
85 public static final Object CATCHUP_LOCK = new Object();
86
87 private static final int LED_COLOR = 0xff00ff00;
88
89 private static final long[] CALL_PATTERN = {0, 500, 300, 600};
90
91 private static final String CONVERSATIONS_GROUP = "eu.siacs.conversations";
92 private static final int NOTIFICATION_ID_MULTIPLIER = 1024 * 1024;
93 static final int FOREGROUND_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 4;
94 private static final int NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 2;
95 private static final int ERROR_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 6;
96 private static final int INCOMING_CALL_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 8;
97 public static final int ONGOING_CALL_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 10;
98 private static final int DELIVERY_FAILED_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 12;
99 private final XmppConnectionService mXmppConnectionService;
100 private final LinkedHashMap<String, ArrayList<Message>> notifications = new LinkedHashMap<>();
101 private final HashMap<Conversation, AtomicInteger> mBacklogMessageCounter = new HashMap<>();
102 private Conversation mOpenConversation;
103 private boolean mIsInForeground;
104 private long mLastNotification;
105
106 private static final String INCOMING_CALLS_NOTIFICATION_CHANNEL = "incoming_calls_channel";
107 private Ringtone currentlyPlayingRingtone = null;
108 private ScheduledFuture<?> vibrationFuture;
109
110 NotificationService(final XmppConnectionService service) {
111 this.mXmppConnectionService = service;
112 }
113
114 private static boolean displaySnoozeAction(List<Message> messages) {
115 int numberOfMessagesWithoutReply = 0;
116 for (Message message : messages) {
117 if (message.getStatus() == Message.STATUS_RECEIVED) {
118 ++numberOfMessagesWithoutReply;
119 } else {
120 return false;
121 }
122 }
123 return numberOfMessagesWithoutReply >= 3;
124 }
125
126 public static Pattern generateNickHighlightPattern(final String nick) {
127 return Pattern.compile("(?<=(^|\\s))" + Pattern.quote(nick) + "(?=\\s|$|\\p{Punct})");
128 }
129
130 private static boolean isImageMessage(Message message) {
131 return message.getType() != Message.TYPE_TEXT
132 && message.getTransferable() == null
133 && !message.isDeleted()
134 && message.getEncryption() != Message.ENCRYPTION_PGP
135 && message.getFileParams().height > 0;
136 }
137
138 @RequiresApi(api = Build.VERSION_CODES.O)
139 void initializeChannels() {
140 final Context c = mXmppConnectionService;
141 final NotificationManager notificationManager =
142 c.getSystemService(NotificationManager.class);
143 if (notificationManager == null) {
144 return;
145 }
146
147 notificationManager.deleteNotificationChannel("export");
148 notificationManager.deleteNotificationChannel("incoming_calls");
149
150 notificationManager.createNotificationChannelGroup(
151 new NotificationChannelGroup(
152 "status", c.getString(R.string.notification_group_status_information)));
153 notificationManager.createNotificationChannelGroup(
154 new NotificationChannelGroup(
155 "chats", c.getString(R.string.notification_group_messages)));
156 notificationManager.createNotificationChannelGroup(
157 new NotificationChannelGroup(
158 "calls", c.getString(R.string.notification_group_calls)));
159 final NotificationChannel foregroundServiceChannel =
160 new NotificationChannel(
161 "foreground",
162 c.getString(R.string.foreground_service_channel_name),
163 NotificationManager.IMPORTANCE_MIN);
164 foregroundServiceChannel.setDescription(
165 c.getString(
166 R.string.foreground_service_channel_description,
167 c.getString(R.string.app_name)));
168 foregroundServiceChannel.setShowBadge(false);
169 foregroundServiceChannel.setGroup("status");
170 notificationManager.createNotificationChannel(foregroundServiceChannel);
171 final NotificationChannel errorChannel =
172 new NotificationChannel(
173 "error",
174 c.getString(R.string.error_channel_name),
175 NotificationManager.IMPORTANCE_LOW);
176 errorChannel.setDescription(c.getString(R.string.error_channel_description));
177 errorChannel.setShowBadge(false);
178 errorChannel.setGroup("status");
179 notificationManager.createNotificationChannel(errorChannel);
180
181 final NotificationChannel videoCompressionChannel =
182 new NotificationChannel(
183 "compression",
184 c.getString(R.string.video_compression_channel_name),
185 NotificationManager.IMPORTANCE_LOW);
186 videoCompressionChannel.setShowBadge(false);
187 videoCompressionChannel.setGroup("status");
188 notificationManager.createNotificationChannel(videoCompressionChannel);
189
190 final NotificationChannel exportChannel =
191 new NotificationChannel(
192 "backup",
193 c.getString(R.string.backup_channel_name),
194 NotificationManager.IMPORTANCE_LOW);
195 exportChannel.setShowBadge(false);
196 exportChannel.setGroup("status");
197 notificationManager.createNotificationChannel(exportChannel);
198
199 final NotificationChannel incomingCallsChannel =
200 new NotificationChannel(
201 INCOMING_CALLS_NOTIFICATION_CHANNEL,
202 c.getString(R.string.incoming_calls_channel_name),
203 NotificationManager.IMPORTANCE_HIGH);
204 incomingCallsChannel.setSound(null, null);
205 incomingCallsChannel.setShowBadge(false);
206 incomingCallsChannel.setLightColor(LED_COLOR);
207 incomingCallsChannel.enableLights(true);
208 incomingCallsChannel.setGroup("calls");
209 incomingCallsChannel.setBypassDnd(true);
210 incomingCallsChannel.enableVibration(false);
211 notificationManager.createNotificationChannel(incomingCallsChannel);
212
213 final NotificationChannel ongoingCallsChannel =
214 new NotificationChannel(
215 "ongoing_calls",
216 c.getString(R.string.ongoing_calls_channel_name),
217 NotificationManager.IMPORTANCE_LOW);
218 ongoingCallsChannel.setShowBadge(false);
219 ongoingCallsChannel.setGroup("calls");
220 notificationManager.createNotificationChannel(ongoingCallsChannel);
221
222 final NotificationChannel messagesChannel =
223 new NotificationChannel(
224 "messages",
225 c.getString(R.string.messages_channel_name),
226 NotificationManager.IMPORTANCE_HIGH);
227 messagesChannel.setShowBadge(true);
228 messagesChannel.setSound(
229 RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION),
230 new AudioAttributes.Builder()
231 .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
232 .setUsage(AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT)
233 .build());
234 messagesChannel.setLightColor(LED_COLOR);
235 final int dat = 70;
236 final long[] pattern = {0, 3 * dat, dat, dat};
237 messagesChannel.setVibrationPattern(pattern);
238 messagesChannel.enableVibration(true);
239 messagesChannel.enableLights(true);
240 messagesChannel.setGroup("chats");
241 notificationManager.createNotificationChannel(messagesChannel);
242 final NotificationChannel silentMessagesChannel =
243 new NotificationChannel(
244 "silent_messages",
245 c.getString(R.string.silent_messages_channel_name),
246 NotificationManager.IMPORTANCE_LOW);
247 silentMessagesChannel.setDescription(
248 c.getString(R.string.silent_messages_channel_description));
249 silentMessagesChannel.setShowBadge(true);
250 silentMessagesChannel.setLightColor(LED_COLOR);
251 silentMessagesChannel.enableLights(true);
252 silentMessagesChannel.setGroup("chats");
253 notificationManager.createNotificationChannel(silentMessagesChannel);
254
255 final NotificationChannel quietHoursChannel =
256 new NotificationChannel(
257 "quiet_hours",
258 c.getString(R.string.title_pref_quiet_hours),
259 NotificationManager.IMPORTANCE_LOW);
260 quietHoursChannel.setShowBadge(true);
261 quietHoursChannel.setLightColor(LED_COLOR);
262 quietHoursChannel.enableLights(true);
263 quietHoursChannel.setGroup("chats");
264 quietHoursChannel.enableVibration(false);
265 quietHoursChannel.setSound(null, null);
266
267 notificationManager.createNotificationChannel(quietHoursChannel);
268
269 final NotificationChannel deliveryFailedChannel =
270 new NotificationChannel(
271 "delivery_failed",
272 c.getString(R.string.delivery_failed_channel_name),
273 NotificationManager.IMPORTANCE_DEFAULT);
274 deliveryFailedChannel.setShowBadge(false);
275 deliveryFailedChannel.setSound(
276 RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION),
277 new AudioAttributes.Builder()
278 .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
279 .setUsage(AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT)
280 .build());
281 deliveryFailedChannel.setGroup("chats");
282 notificationManager.createNotificationChannel(deliveryFailedChannel);
283 }
284
285 private boolean notify(final Message message) {
286 final Conversation conversation = (Conversation) message.getConversation();
287 return message.getStatus() == Message.STATUS_RECEIVED
288 && !conversation.isMuted()
289 && (conversation.alwaysNotify() || wasHighlightedOrPrivate(message))
290 && (!conversation.isWithStranger() || notificationsFromStrangers());
291 }
292
293 public boolean notificationsFromStrangers() {
294 return mXmppConnectionService.getBooleanPreference(
295 "notifications_from_strangers", R.bool.notifications_from_strangers);
296 }
297
298 private boolean isQuietHours() {
299 if (!mXmppConnectionService.getBooleanPreference(
300 "enable_quiet_hours", R.bool.enable_quiet_hours)) {
301 return false;
302 }
303 final SharedPreferences preferences =
304 PreferenceManager.getDefaultSharedPreferences(mXmppConnectionService);
305 final long startTime =
306 TimePreference.minutesToTimestamp(
307 preferences.getLong("quiet_hours_start", TimePreference.DEFAULT_VALUE));
308 final long endTime =
309 TimePreference.minutesToTimestamp(
310 preferences.getLong("quiet_hours_end", TimePreference.DEFAULT_VALUE));
311 final long nowTime = Calendar.getInstance().getTimeInMillis();
312
313 if (endTime < startTime) {
314 return nowTime > startTime || nowTime < endTime;
315 } else {
316 return nowTime > startTime && nowTime < endTime;
317 }
318 }
319
320 public void pushFromBacklog(final Message message) {
321 if (notify(message)) {
322 synchronized (notifications) {
323 getBacklogMessageCounter((Conversation) message.getConversation())
324 .incrementAndGet();
325 pushToStack(message);
326 }
327 }
328 }
329
330 private AtomicInteger getBacklogMessageCounter(Conversation conversation) {
331 synchronized (mBacklogMessageCounter) {
332 if (!mBacklogMessageCounter.containsKey(conversation)) {
333 mBacklogMessageCounter.put(conversation, new AtomicInteger(0));
334 }
335 return mBacklogMessageCounter.get(conversation);
336 }
337 }
338
339 void pushFromDirectReply(final Message message) {
340 synchronized (notifications) {
341 pushToStack(message);
342 updateNotification(false);
343 }
344 }
345
346 public void finishBacklog(boolean notify, Account account) {
347 synchronized (notifications) {
348 mXmppConnectionService.updateUnreadCountBadge();
349 if (account == null || !notify) {
350 updateNotification(notify);
351 } else {
352 final int count;
353 final List<String> conversations;
354 synchronized (this.mBacklogMessageCounter) {
355 conversations = getBacklogConversations(account);
356 count = getBacklogMessageCount(account);
357 }
358 updateNotification(count > 0, conversations);
359 }
360 }
361 }
362
363 private List<String> getBacklogConversations(Account account) {
364 final List<String> conversations = new ArrayList<>();
365 for (Map.Entry<Conversation, AtomicInteger> entry : mBacklogMessageCounter.entrySet()) {
366 if (entry.getKey().getAccount() == account) {
367 conversations.add(entry.getKey().getUuid());
368 }
369 }
370 return conversations;
371 }
372
373 private int getBacklogMessageCount(Account account) {
374 int count = 0;
375 for (Iterator<Map.Entry<Conversation, AtomicInteger>> it =
376 mBacklogMessageCounter.entrySet().iterator();
377 it.hasNext(); ) {
378 Map.Entry<Conversation, AtomicInteger> entry = it.next();
379 if (entry.getKey().getAccount() == account) {
380 count += entry.getValue().get();
381 it.remove();
382 }
383 }
384 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": backlog message count=" + count);
385 return count;
386 }
387
388 void finishBacklog(boolean notify) {
389 finishBacklog(notify, null);
390 }
391
392 private void pushToStack(final Message message) {
393 final String conversationUuid = message.getConversationUuid();
394 if (notifications.containsKey(conversationUuid)) {
395 notifications.get(conversationUuid).add(message);
396 } else {
397 final ArrayList<Message> mList = new ArrayList<>();
398 mList.add(message);
399 notifications.put(conversationUuid, mList);
400 }
401 }
402
403 public void push(final Message message) {
404 synchronized (CATCHUP_LOCK) {
405 final XmppConnection connection =
406 message.getConversation().getAccount().getXmppConnection();
407 if (connection != null && connection.isWaitingForSmCatchup()) {
408 connection.incrementSmCatchupMessageCounter();
409 pushFromBacklog(message);
410 } else {
411 pushNow(message);
412 }
413 }
414 }
415
416 public void pushFailedDelivery(final Message message) {
417 final Conversation conversation = (Conversation) message.getConversation();
418 final boolean isScreenLocked = !mXmppConnectionService.isScreenLocked();
419 if (this.mIsInForeground
420 && isScreenLocked
421 && this.mOpenConversation == message.getConversation()) {
422 Log.d(
423 Config.LOGTAG,
424 message.getConversation().getAccount().getJid().asBareJid()
425 + ": suppressing failed delivery notification because conversation is open");
426 return;
427 }
428 final PendingIntent pendingIntent = createContentIntent(conversation);
429 final int notificationId =
430 generateRequestCode(conversation, 0) + DELIVERY_FAILED_NOTIFICATION_ID;
431 final int failedDeliveries = conversation.countFailedDeliveries();
432 final Notification notification =
433 new Builder(mXmppConnectionService, "delivery_failed")
434 .setContentTitle(conversation.getName())
435 .setAutoCancel(true)
436 .setSmallIcon(R.drawable.ic_error_white_24dp)
437 .setContentText(
438 mXmppConnectionService
439 .getResources()
440 .getQuantityText(
441 R.plurals.some_messages_could_not_be_delivered,
442 failedDeliveries))
443 .setGroup("delivery_failed")
444 .setContentIntent(pendingIntent)
445 .build();
446 final Notification summaryNotification =
447 new Builder(mXmppConnectionService, "delivery_failed")
448 .setContentTitle(
449 mXmppConnectionService.getString(R.string.failed_deliveries))
450 .setContentText(
451 mXmppConnectionService
452 .getResources()
453 .getQuantityText(
454 R.plurals.some_messages_could_not_be_delivered,
455 1024))
456 .setSmallIcon(R.drawable.ic_error_white_24dp)
457 .setGroup("delivery_failed")
458 .setGroupSummary(true)
459 .setAutoCancel(true)
460 .build();
461 notify(notificationId, notification);
462 notify(DELIVERY_FAILED_NOTIFICATION_ID, summaryNotification);
463 }
464
465 public synchronized void startRinging(
466 final AbstractJingleConnection.Id id, final Set<Media> media) {
467 showIncomingCallNotification(id, media);
468 final NotificationManager notificationManager =
469 (NotificationManager)
470 mXmppConnectionService.getSystemService(Context.NOTIFICATION_SERVICE);
471 final int currentInterruptionFilter;
472 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && notificationManager != null) {
473 currentInterruptionFilter = notificationManager.getCurrentInterruptionFilter();
474 } else {
475 currentInterruptionFilter = 1; // INTERRUPTION_FILTER_ALL
476 }
477 if (currentInterruptionFilter != 1) {
478 Log.d(
479 Config.LOGTAG,
480 "do not ring or vibrate because interruption filter has been set to "
481 + currentInterruptionFilter);
482 return;
483 }
484 final ScheduledFuture<?> currentVibrationFuture = this.vibrationFuture;
485 this.vibrationFuture =
486 SCHEDULED_EXECUTOR_SERVICE.scheduleAtFixedRate(
487 new VibrationRunnable(), 0, 3, TimeUnit.SECONDS);
488 if (currentVibrationFuture != null) {
489 currentVibrationFuture.cancel(true);
490 }
491 final SharedPreferences preferences =
492 PreferenceManager.getDefaultSharedPreferences(mXmppConnectionService);
493 final Resources resources = mXmppConnectionService.getResources();
494 final String ringtonePreference =
495 preferences.getString(
496 "call_ringtone", resources.getString(R.string.incoming_call_ringtone));
497 if (Strings.isNullOrEmpty(ringtonePreference)) {
498 Log.d(Config.LOGTAG, "ringtone has been set to none");
499 return;
500 }
501 final Uri uri = Uri.parse(ringtonePreference);
502 this.currentlyPlayingRingtone = RingtoneManager.getRingtone(mXmppConnectionService, uri);
503 if (this.currentlyPlayingRingtone == null) {
504 Log.d(Config.LOGTAG, "unable to find ringtone for uri " + uri);
505 return;
506 }
507 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
508 this.currentlyPlayingRingtone.setLooping(true);
509 }
510 this.currentlyPlayingRingtone.play();
511 }
512
513 private void showIncomingCallNotification(
514 final AbstractJingleConnection.Id id, final Set<Media> media) {
515 final Intent fullScreenIntent =
516 new Intent(mXmppConnectionService, RtpSessionActivity.class);
517 fullScreenIntent.putExtra(
518 RtpSessionActivity.EXTRA_ACCOUNT,
519 id.account.getJid().asBareJid().toEscapedString());
520 fullScreenIntent.putExtra(RtpSessionActivity.EXTRA_WITH, id.with.toEscapedString());
521 fullScreenIntent.putExtra(RtpSessionActivity.EXTRA_SESSION_ID, id.sessionId);
522 fullScreenIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
523 fullScreenIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
524 final NotificationCompat.Builder builder =
525 new NotificationCompat.Builder(
526 mXmppConnectionService, INCOMING_CALLS_NOTIFICATION_CHANNEL);
527 if (media.contains(Media.VIDEO)) {
528 builder.setSmallIcon(R.drawable.ic_videocam_white_24dp);
529 builder.setContentTitle(
530 mXmppConnectionService.getString(R.string.rtp_state_incoming_video_call));
531 } else {
532 builder.setSmallIcon(R.drawable.ic_call_white_24dp);
533 builder.setContentTitle(
534 mXmppConnectionService.getString(R.string.rtp_state_incoming_call));
535 }
536 final Contact contact = id.getContact();
537 builder.setLargeIcon(
538 mXmppConnectionService
539 .getAvatarService()
540 .get(contact, AvatarService.getSystemUiAvatarSize(mXmppConnectionService)));
541 final Uri systemAccount = contact.getSystemAccount();
542 if (systemAccount != null) {
543 builder.addPerson(systemAccount.toString());
544 }
545 builder.setContentText(id.account.getRoster().getContact(id.with).getDisplayName());
546 builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC);
547 builder.setPriority(NotificationCompat.PRIORITY_HIGH);
548 builder.setCategory(NotificationCompat.CATEGORY_CALL);
549 PendingIntent pendingIntent = createPendingRtpSession(id, Intent.ACTION_VIEW, 101);
550 builder.setFullScreenIntent(pendingIntent, true);
551 builder.setContentIntent(pendingIntent); // old androids need this?
552 builder.setOngoing(true);
553 builder.addAction(
554 new NotificationCompat.Action.Builder(
555 R.drawable.ic_call_end_white_48dp,
556 mXmppConnectionService.getString(R.string.dismiss_call),
557 createCallAction(
558 id.sessionId,
559 XmppConnectionService.ACTION_DISMISS_CALL,
560 102))
561 .build());
562 builder.addAction(
563 new NotificationCompat.Action.Builder(
564 R.drawable.ic_call_white_24dp,
565 mXmppConnectionService.getString(R.string.answer_call),
566 createPendingRtpSession(
567 id, RtpSessionActivity.ACTION_ACCEPT_CALL, 103))
568 .build());
569 modifyIncomingCall(builder);
570 final Notification notification = builder.build();
571 notification.flags = notification.flags | Notification.FLAG_INSISTENT;
572 notify(INCOMING_CALL_NOTIFICATION_ID, notification);
573 }
574
575 public Notification getOngoingCallNotification(
576 final XmppConnectionService.OngoingCall ongoingCall) {
577 final AbstractJingleConnection.Id id = ongoingCall.id;
578 final NotificationCompat.Builder builder =
579 new NotificationCompat.Builder(mXmppConnectionService, "ongoing_calls");
580 if (ongoingCall.media.contains(Media.VIDEO)) {
581 builder.setSmallIcon(R.drawable.ic_videocam_white_24dp);
582 if (ongoingCall.reconnecting) {
583 builder.setContentTitle(
584 mXmppConnectionService.getString(R.string.reconnecting_video_call));
585 } else {
586 builder.setContentTitle(
587 mXmppConnectionService.getString(R.string.ongoing_video_call));
588 }
589 } else {
590 builder.setSmallIcon(R.drawable.ic_call_white_24dp);
591 if (ongoingCall.reconnecting) {
592 builder.setContentTitle(
593 mXmppConnectionService.getString(R.string.reconnecting_call));
594 } else {
595 builder.setContentTitle(mXmppConnectionService.getString(R.string.ongoing_call));
596 }
597 }
598 builder.setContentText(id.account.getRoster().getContact(id.with).getDisplayName());
599 builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC);
600 builder.setPriority(NotificationCompat.PRIORITY_HIGH);
601 builder.setCategory(NotificationCompat.CATEGORY_CALL);
602 builder.setContentIntent(createPendingRtpSession(id, Intent.ACTION_VIEW, 101));
603 builder.setOngoing(true);
604 builder.addAction(
605 new NotificationCompat.Action.Builder(
606 R.drawable.ic_call_end_white_48dp,
607 mXmppConnectionService.getString(R.string.hang_up),
608 createCallAction(
609 id.sessionId, XmppConnectionService.ACTION_END_CALL, 104))
610 .build());
611 return builder.build();
612 }
613
614 private PendingIntent createPendingRtpSession(
615 final AbstractJingleConnection.Id id, final String action, final int requestCode) {
616 final Intent fullScreenIntent =
617 new Intent(mXmppConnectionService, RtpSessionActivity.class);
618 fullScreenIntent.setAction(action);
619 fullScreenIntent.putExtra(
620 RtpSessionActivity.EXTRA_ACCOUNT,
621 id.account.getJid().asBareJid().toEscapedString());
622 fullScreenIntent.putExtra(RtpSessionActivity.EXTRA_WITH, id.with.toEscapedString());
623 fullScreenIntent.putExtra(RtpSessionActivity.EXTRA_SESSION_ID, id.sessionId);
624 return PendingIntent.getActivity(
625 mXmppConnectionService,
626 requestCode,
627 fullScreenIntent,
628 PendingIntent.FLAG_UPDATE_CURRENT);
629 }
630
631 public void cancelIncomingCallNotification() {
632 stopSoundAndVibration();
633 cancel(INCOMING_CALL_NOTIFICATION_ID);
634 }
635
636 public boolean stopSoundAndVibration() {
637 int stopped = 0;
638 if (this.currentlyPlayingRingtone != null) {
639 if (this.currentlyPlayingRingtone.isPlaying()) {
640 Log.d(Config.LOGTAG, "stop playing ring tone");
641 ++stopped;
642 }
643 this.currentlyPlayingRingtone.stop();
644 }
645 if (this.vibrationFuture != null && !this.vibrationFuture.isCancelled()) {
646 Log.d(Config.LOGTAG, "stop vibration");
647 this.vibrationFuture.cancel(true);
648 ++stopped;
649 }
650 return stopped > 0;
651 }
652
653 public static void cancelIncomingCallNotification(final Context context) {
654 final NotificationManagerCompat notificationManager =
655 NotificationManagerCompat.from(context);
656 try {
657 notificationManager.cancel(INCOMING_CALL_NOTIFICATION_ID);
658 } catch (RuntimeException e) {
659 Log.d(Config.LOGTAG, "unable to cancel incoming call notification after crash", e);
660 }
661 }
662
663 private void pushNow(final Message message) {
664 mXmppConnectionService.updateUnreadCountBadge();
665 if (!notify(message)) {
666 Log.d(
667 Config.LOGTAG,
668 message.getConversation().getAccount().getJid().asBareJid()
669 + ": suppressing notification because turned off");
670 return;
671 }
672 final boolean isScreenLocked = mXmppConnectionService.isScreenLocked();
673 if (this.mIsInForeground
674 && !isScreenLocked
675 && this.mOpenConversation == message.getConversation()) {
676 Log.d(
677 Config.LOGTAG,
678 message.getConversation().getAccount().getJid().asBareJid()
679 + ": suppressing notification because conversation is open");
680 return;
681 }
682 synchronized (notifications) {
683 pushToStack(message);
684 final Conversational conversation = message.getConversation();
685 final Account account = conversation.getAccount();
686 final boolean doNotify =
687 (!(this.mIsInForeground && this.mOpenConversation == null) || isScreenLocked)
688 && !account.inGracePeriod()
689 && !this.inMiniGracePeriod(account);
690 updateNotification(doNotify, Collections.singletonList(conversation.getUuid()));
691 }
692 }
693
694 public void clear() {
695 synchronized (notifications) {
696 for (ArrayList<Message> messages : notifications.values()) {
697 markAsReadIfHasDirectReply(messages);
698 }
699 notifications.clear();
700 updateNotification(false);
701 }
702 }
703
704 public void clear(final Conversation conversation) {
705 synchronized (this.mBacklogMessageCounter) {
706 this.mBacklogMessageCounter.remove(conversation);
707 }
708 synchronized (notifications) {
709 markAsReadIfHasDirectReply(conversation);
710 if (notifications.remove(conversation.getUuid()) != null) {
711 cancel(conversation.getUuid(), NOTIFICATION_ID);
712 updateNotification(false, null, true);
713 }
714 }
715 }
716
717 private void markAsReadIfHasDirectReply(final Conversation conversation) {
718 markAsReadIfHasDirectReply(notifications.get(conversation.getUuid()));
719 }
720
721 private void markAsReadIfHasDirectReply(final ArrayList<Message> messages) {
722 if (messages != null && messages.size() > 0) {
723 Message last = messages.get(messages.size() - 1);
724 if (last.getStatus() != Message.STATUS_RECEIVED) {
725 if (mXmppConnectionService.markRead((Conversation) last.getConversation(), false)) {
726 mXmppConnectionService.updateConversationUi();
727 }
728 }
729 }
730 }
731
732 private void setNotificationColor(final Builder mBuilder) {
733 mBuilder.setColor(ContextCompat.getColor(mXmppConnectionService, R.color.green600));
734 }
735
736 public void updateNotification() {
737 synchronized (notifications) {
738 updateNotification(false);
739 }
740 }
741
742 private void updateNotification(final boolean notify) {
743 updateNotification(notify, null, false);
744 }
745
746 private void updateNotification(final boolean notify, final List<String> conversations) {
747 updateNotification(notify, conversations, false);
748 }
749
750 private void updateNotification(
751 final boolean notify, final List<String> conversations, final boolean summaryOnly) {
752 final SharedPreferences preferences =
753 PreferenceManager.getDefaultSharedPreferences(mXmppConnectionService);
754
755 final boolean quiteHours = isQuietHours();
756
757 final boolean notifyOnlyOneChild =
758 notify
759 && conversations != null
760 && conversations.size()
761 == 1; // if this check is changed to > 0 catchup messages will
762 // create one notification per conversation
763
764 if (notifications.size() == 0) {
765 cancel(NOTIFICATION_ID);
766 } else {
767 if (notify) {
768 this.markLastNotification();
769 }
770 final Builder mBuilder;
771 if (notifications.size() == 1 && Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
772 mBuilder =
773 buildSingleConversations(
774 notifications.values().iterator().next(), notify, quiteHours);
775 modifyForSoundVibrationAndLight(mBuilder, notify, quiteHours, preferences);
776 notify(NOTIFICATION_ID, mBuilder.build());
777 } else {
778 mBuilder = buildMultipleConversation(notify, quiteHours);
779 if (notifyOnlyOneChild) {
780 mBuilder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN);
781 }
782 modifyForSoundVibrationAndLight(mBuilder, notify, quiteHours, preferences);
783 if (!summaryOnly) {
784 for (Map.Entry<String, ArrayList<Message>> entry : notifications.entrySet()) {
785 String uuid = entry.getKey();
786 final boolean notifyThis =
787 notifyOnlyOneChild ? conversations.contains(uuid) : notify;
788 Builder singleBuilder =
789 buildSingleConversations(entry.getValue(), notifyThis, quiteHours);
790 if (!notifyOnlyOneChild) {
791 singleBuilder.setGroupAlertBehavior(
792 NotificationCompat.GROUP_ALERT_SUMMARY);
793 }
794 modifyForSoundVibrationAndLight(
795 singleBuilder, notifyThis, quiteHours, preferences);
796 singleBuilder.setGroup(CONVERSATIONS_GROUP);
797 setNotificationColor(singleBuilder);
798 notify(entry.getKey(), NOTIFICATION_ID, singleBuilder.build());
799 }
800 }
801 notify(NOTIFICATION_ID, mBuilder.build());
802 }
803 }
804 }
805
806 private void modifyForSoundVibrationAndLight(
807 Builder mBuilder, boolean notify, boolean quietHours, SharedPreferences preferences) {
808 final Resources resources = mXmppConnectionService.getResources();
809 final String ringtone =
810 preferences.getString(
811 "notification_ringtone",
812 resources.getString(R.string.notification_ringtone));
813 final boolean vibrate =
814 preferences.getBoolean(
815 "vibrate_on_notification",
816 resources.getBoolean(R.bool.vibrate_on_notification));
817 final boolean led = preferences.getBoolean("led", resources.getBoolean(R.bool.led));
818 final boolean headsup =
819 preferences.getBoolean(
820 "notification_headsup", resources.getBoolean(R.bool.headsup_notifications));
821 if (notify && !quietHours) {
822 if (vibrate) {
823 final int dat = 70;
824 final long[] pattern = {0, 3 * dat, dat, dat};
825 mBuilder.setVibrate(pattern);
826 } else {
827 mBuilder.setVibrate(new long[] {0});
828 }
829 Uri uri = Uri.parse(ringtone);
830 try {
831 mBuilder.setSound(fixRingtoneUri(uri));
832 } catch (SecurityException e) {
833 Log.d(Config.LOGTAG, "unable to use custom notification sound " + uri.toString());
834 }
835 } else {
836 mBuilder.setLocalOnly(true);
837 }
838 if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
839 mBuilder.setCategory(Notification.CATEGORY_MESSAGE);
840 }
841 mBuilder.setPriority(
842 notify
843 ? (headsup
844 ? NotificationCompat.PRIORITY_HIGH
845 : NotificationCompat.PRIORITY_DEFAULT)
846 : NotificationCompat.PRIORITY_LOW);
847 setNotificationColor(mBuilder);
848 mBuilder.setDefaults(0);
849 if (led) {
850 mBuilder.setLights(LED_COLOR, 2000, 3000);
851 }
852 }
853
854 private void modifyIncomingCall(final Builder mBuilder) {
855 mBuilder.setPriority(NotificationCompat.PRIORITY_HIGH);
856 setNotificationColor(mBuilder);
857 mBuilder.setLights(LED_COLOR, 2000, 3000);
858 }
859
860 private Uri fixRingtoneUri(Uri uri) {
861 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && "file".equals(uri.getScheme())) {
862 return FileBackend.getUriForFile(mXmppConnectionService, new File(uri.getPath()));
863 } else {
864 return uri;
865 }
866 }
867
868 private Builder buildMultipleConversation(final boolean notify, final boolean quietHours) {
869 final Builder mBuilder =
870 new NotificationCompat.Builder(
871 mXmppConnectionService,
872 quietHours ? "quiet_hours" : (notify ? "messages" : "silent_messages"));
873 final NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle();
874 style.setBigContentTitle(
875 mXmppConnectionService
876 .getResources()
877 .getQuantityString(
878 R.plurals.x_unread_conversations,
879 notifications.size(),
880 notifications.size()));
881 final StringBuilder names = new StringBuilder();
882 Conversation conversation = null;
883 for (final ArrayList<Message> messages : notifications.values()) {
884 if (messages.size() > 0) {
885 conversation = (Conversation) messages.get(0).getConversation();
886 final String name = conversation.getName().toString();
887 SpannableString styledString;
888 if (Config.HIDE_MESSAGE_TEXT_IN_NOTIFICATION) {
889 int count = messages.size();
890 styledString =
891 new SpannableString(
892 name
893 + ": "
894 + mXmppConnectionService
895 .getResources()
896 .getQuantityString(
897 R.plurals.x_messages, count, count));
898 styledString.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), 0);
899 style.addLine(styledString);
900 } else {
901 styledString =
902 new SpannableString(
903 name
904 + ": "
905 + UIHelper.getMessagePreview(
906 mXmppConnectionService, messages.get(0))
907 .first);
908 styledString.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), 0);
909 style.addLine(styledString);
910 }
911 names.append(name);
912 names.append(", ");
913 }
914 }
915 if (names.length() >= 2) {
916 names.delete(names.length() - 2, names.length());
917 }
918 final String contentTitle =
919 mXmppConnectionService
920 .getResources()
921 .getQuantityString(
922 R.plurals.x_unread_conversations,
923 notifications.size(),
924 notifications.size());
925 mBuilder.setContentTitle(contentTitle);
926 mBuilder.setTicker(contentTitle);
927 mBuilder.setContentText(names.toString());
928 mBuilder.setStyle(style);
929 if (conversation != null) {
930 mBuilder.setContentIntent(createContentIntent(conversation));
931 }
932 mBuilder.setGroupSummary(true);
933 mBuilder.setGroup(CONVERSATIONS_GROUP);
934 mBuilder.setDeleteIntent(createDeleteIntent(null));
935 mBuilder.setSmallIcon(R.drawable.ic_notification);
936 return mBuilder;
937 }
938
939 private Builder buildSingleConversations(
940 final ArrayList<Message> messages, final boolean notify, final boolean quietHours) {
941 final Builder mBuilder =
942 new NotificationCompat.Builder(
943 mXmppConnectionService,
944 quietHours ? "quiet_hours" : (notify ? "messages" : "silent_messages"));
945 if (messages.size() >= 1) {
946 final Conversation conversation = (Conversation) messages.get(0).getConversation();
947 mBuilder.setLargeIcon(
948 mXmppConnectionService
949 .getAvatarService()
950 .get(
951 conversation,
952 AvatarService.getSystemUiAvatarSize(mXmppConnectionService)));
953 mBuilder.setContentTitle(conversation.getName());
954 if (Config.HIDE_MESSAGE_TEXT_IN_NOTIFICATION) {
955 int count = messages.size();
956 mBuilder.setContentText(
957 mXmppConnectionService
958 .getResources()
959 .getQuantityString(R.plurals.x_messages, count, count));
960 } else {
961 Message message;
962 // TODO starting with Android 9 we might want to put images in MessageStyle
963 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P
964 && (message = getImage(messages)) != null) {
965 modifyForImage(mBuilder, message, messages);
966 } else {
967 modifyForTextOnly(mBuilder, messages);
968 }
969 RemoteInput remoteInput =
970 new RemoteInput.Builder("text_reply")
971 .setLabel(
972 UIHelper.getMessageHint(
973 mXmppConnectionService, conversation))
974 .build();
975 PendingIntent markAsReadPendingIntent = createReadPendingIntent(conversation);
976 NotificationCompat.Action markReadAction =
977 new NotificationCompat.Action.Builder(
978 R.drawable.ic_drafts_white_24dp,
979 mXmppConnectionService.getString(R.string.mark_as_read),
980 markAsReadPendingIntent)
981 .setSemanticAction(
982 NotificationCompat.Action.SEMANTIC_ACTION_MARK_AS_READ)
983 .setShowsUserInterface(false)
984 .build();
985 final String replyLabel = mXmppConnectionService.getString(R.string.reply);
986 final String lastMessageUuid = Iterables.getLast(messages).getUuid();
987 final NotificationCompat.Action replyAction =
988 new NotificationCompat.Action.Builder(
989 R.drawable.ic_send_text_offline,
990 replyLabel,
991 createReplyIntent(conversation, lastMessageUuid, false))
992 .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY)
993 .setShowsUserInterface(false)
994 .addRemoteInput(remoteInput)
995 .build();
996 final NotificationCompat.Action wearReplyAction =
997 new NotificationCompat.Action.Builder(
998 R.drawable.ic_wear_reply,
999 replyLabel,
1000 createReplyIntent(conversation, lastMessageUuid, true))
1001 .addRemoteInput(remoteInput)
1002 .build();
1003 mBuilder.extend(
1004 new NotificationCompat.WearableExtender().addAction(wearReplyAction));
1005 int addedActionsCount = 1;
1006 mBuilder.addAction(markReadAction);
1007 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
1008 mBuilder.addAction(replyAction);
1009 ++addedActionsCount;
1010 }
1011
1012 if (displaySnoozeAction(messages)) {
1013 String label = mXmppConnectionService.getString(R.string.snooze);
1014 PendingIntent pendingSnoozeIntent = createSnoozeIntent(conversation);
1015 NotificationCompat.Action snoozeAction =
1016 new NotificationCompat.Action.Builder(
1017 R.drawable.ic_notifications_paused_white_24dp,
1018 label,
1019 pendingSnoozeIntent)
1020 .build();
1021 mBuilder.addAction(snoozeAction);
1022 ++addedActionsCount;
1023 }
1024 if (addedActionsCount < 3) {
1025 final Message firstLocationMessage = getFirstLocationMessage(messages);
1026 if (firstLocationMessage != null) {
1027 final PendingIntent pendingShowLocationIntent =
1028 createShowLocationIntent(firstLocationMessage);
1029 if (pendingShowLocationIntent != null) {
1030 final String label =
1031 mXmppConnectionService
1032 .getResources()
1033 .getString(R.string.show_location);
1034 NotificationCompat.Action locationAction =
1035 new NotificationCompat.Action.Builder(
1036 R.drawable.ic_room_white_24dp,
1037 label,
1038 pendingShowLocationIntent)
1039 .build();
1040 mBuilder.addAction(locationAction);
1041 ++addedActionsCount;
1042 }
1043 }
1044 }
1045 if (addedActionsCount < 3) {
1046 Message firstDownloadableMessage = getFirstDownloadableMessage(messages);
1047 if (firstDownloadableMessage != null) {
1048 String label =
1049 mXmppConnectionService
1050 .getResources()
1051 .getString(
1052 R.string.download_x_file,
1053 UIHelper.getFileDescriptionString(
1054 mXmppConnectionService,
1055 firstDownloadableMessage));
1056 PendingIntent pendingDownloadIntent =
1057 createDownloadIntent(firstDownloadableMessage);
1058 NotificationCompat.Action downloadAction =
1059 new NotificationCompat.Action.Builder(
1060 R.drawable.ic_file_download_white_24dp,
1061 label,
1062 pendingDownloadIntent)
1063 .build();
1064 mBuilder.addAction(downloadAction);
1065 ++addedActionsCount;
1066 }
1067 }
1068 }
1069 if (conversation.getMode() == Conversation.MODE_SINGLE) {
1070 Contact contact = conversation.getContact();
1071 Uri systemAccount = contact.getSystemAccount();
1072 if (systemAccount != null) {
1073 mBuilder.addPerson(systemAccount.toString());
1074 }
1075 }
1076 mBuilder.setWhen(conversation.getLatestMessage().getTimeSent());
1077 mBuilder.setSmallIcon(R.drawable.ic_notification);
1078 mBuilder.setDeleteIntent(createDeleteIntent(conversation));
1079 mBuilder.setContentIntent(createContentIntent(conversation));
1080 }
1081 return mBuilder;
1082 }
1083
1084 private void modifyForImage(
1085 final Builder builder, final Message message, final ArrayList<Message> messages) {
1086 try {
1087 final Bitmap bitmap =
1088 mXmppConnectionService
1089 .getFileBackend()
1090 .getThumbnail(message, getPixel(288), false);
1091 final ArrayList<Message> tmp = new ArrayList<>();
1092 for (final Message msg : messages) {
1093 if (msg.getType() == Message.TYPE_TEXT && msg.getTransferable() == null) {
1094 tmp.add(msg);
1095 }
1096 }
1097 final BigPictureStyle bigPictureStyle = new NotificationCompat.BigPictureStyle();
1098 bigPictureStyle.bigPicture(bitmap);
1099 if (tmp.size() > 0) {
1100 CharSequence text = getMergedBodies(tmp);
1101 bigPictureStyle.setSummaryText(text);
1102 builder.setContentText(text);
1103 builder.setTicker(text);
1104 } else {
1105 final String description =
1106 UIHelper.getFileDescriptionString(mXmppConnectionService, message);
1107 builder.setContentText(description);
1108 builder.setTicker(description);
1109 }
1110 builder.setStyle(bigPictureStyle);
1111 } catch (final IOException e) {
1112 modifyForTextOnly(builder, messages);
1113 }
1114 }
1115
1116 private Person getPerson(Message message) {
1117 final Contact contact = message.getContact();
1118 final Person.Builder builder = new Person.Builder();
1119 if (contact != null) {
1120 builder.setName(contact.getDisplayName());
1121 final Uri uri = contact.getSystemAccount();
1122 if (uri != null) {
1123 builder.setUri(uri.toString());
1124 }
1125 } else {
1126 builder.setName(UIHelper.getMessageDisplayName(message));
1127 }
1128 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
1129 builder.setIcon(
1130 IconCompat.createWithBitmap(
1131 mXmppConnectionService
1132 .getAvatarService()
1133 .get(
1134 message,
1135 AvatarService.getSystemUiAvatarSize(
1136 mXmppConnectionService),
1137 false)));
1138 }
1139 return builder.build();
1140 }
1141
1142 private void modifyForTextOnly(final Builder builder, final ArrayList<Message> messages) {
1143 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
1144 final Conversation conversation = (Conversation) messages.get(0).getConversation();
1145 final Person.Builder meBuilder =
1146 new Person.Builder().setName(mXmppConnectionService.getString(R.string.me));
1147 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
1148 meBuilder.setIcon(
1149 IconCompat.createWithBitmap(
1150 mXmppConnectionService
1151 .getAvatarService()
1152 .get(
1153 conversation.getAccount(),
1154 AvatarService.getSystemUiAvatarSize(
1155 mXmppConnectionService))));
1156 }
1157 final Person me = meBuilder.build();
1158 NotificationCompat.MessagingStyle messagingStyle =
1159 new NotificationCompat.MessagingStyle(me);
1160 final boolean multiple = conversation.getMode() == Conversation.MODE_MULTI;
1161 if (multiple) {
1162 messagingStyle.setConversationTitle(conversation.getName());
1163 }
1164 for (Message message : messages) {
1165 final Person sender =
1166 message.getStatus() == Message.STATUS_RECEIVED ? getPerson(message) : null;
1167 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && isImageMessage(message)) {
1168 final Uri dataUri =
1169 FileBackend.getMediaUri(
1170 mXmppConnectionService,
1171 mXmppConnectionService.getFileBackend().getFile(message));
1172 NotificationCompat.MessagingStyle.Message imageMessage =
1173 new NotificationCompat.MessagingStyle.Message(
1174 UIHelper.getMessagePreview(mXmppConnectionService, message)
1175 .first,
1176 message.getTimeSent(),
1177 sender);
1178 if (dataUri != null) {
1179 imageMessage.setData(message.getMimeType(), dataUri);
1180 }
1181 messagingStyle.addMessage(imageMessage);
1182 } else {
1183 messagingStyle.addMessage(
1184 UIHelper.getMessagePreview(mXmppConnectionService, message).first,
1185 message.getTimeSent(),
1186 sender);
1187 }
1188 }
1189 messagingStyle.setGroupConversation(multiple);
1190 builder.setStyle(messagingStyle);
1191 } else {
1192 if (messages.get(0).getConversation().getMode() == Conversation.MODE_SINGLE) {
1193 builder.setStyle(
1194 new NotificationCompat.BigTextStyle().bigText(getMergedBodies(messages)));
1195 final CharSequence preview =
1196 UIHelper.getMessagePreview(
1197 mXmppConnectionService, messages.get(messages.size() - 1))
1198 .first;
1199 builder.setContentText(preview);
1200 builder.setTicker(preview);
1201 builder.setNumber(messages.size());
1202 } else {
1203 final NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle();
1204 SpannableString styledString;
1205 for (Message message : messages) {
1206 final String name = UIHelper.getMessageDisplayName(message);
1207 styledString = new SpannableString(name + ": " + message.getBody());
1208 styledString.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), 0);
1209 style.addLine(styledString);
1210 }
1211 builder.setStyle(style);
1212 int count = messages.size();
1213 if (count == 1) {
1214 final String name = UIHelper.getMessageDisplayName(messages.get(0));
1215 styledString = new SpannableString(name + ": " + messages.get(0).getBody());
1216 styledString.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), 0);
1217 builder.setContentText(styledString);
1218 builder.setTicker(styledString);
1219 } else {
1220 final String text =
1221 mXmppConnectionService
1222 .getResources()
1223 .getQuantityString(R.plurals.x_messages, count, count);
1224 builder.setContentText(text);
1225 builder.setTicker(text);
1226 }
1227 }
1228 }
1229 }
1230
1231 private Message getImage(final Iterable<Message> messages) {
1232 Message image = null;
1233 for (final Message message : messages) {
1234 if (message.getStatus() != Message.STATUS_RECEIVED) {
1235 return null;
1236 }
1237 if (isImageMessage(message)) {
1238 image = message;
1239 }
1240 }
1241 return image;
1242 }
1243
1244 private Message getFirstDownloadableMessage(final Iterable<Message> messages) {
1245 for (final Message message : messages) {
1246 if (message.getTransferable() != null
1247 || (message.getType() == Message.TYPE_TEXT && message.treatAsDownloadable())) {
1248 return message;
1249 }
1250 }
1251 return null;
1252 }
1253
1254 private Message getFirstLocationMessage(final Iterable<Message> messages) {
1255 for (final Message message : messages) {
1256 if (message.isGeoUri()) {
1257 return message;
1258 }
1259 }
1260 return null;
1261 }
1262
1263 private CharSequence getMergedBodies(final ArrayList<Message> messages) {
1264 final StringBuilder text = new StringBuilder();
1265 for (Message message : messages) {
1266 if (text.length() != 0) {
1267 text.append("\n");
1268 }
1269 text.append(UIHelper.getMessagePreview(mXmppConnectionService, message).first);
1270 }
1271 return text.toString();
1272 }
1273
1274 private PendingIntent createShowLocationIntent(final Message message) {
1275 Iterable<Intent> intents =
1276 GeoHelper.createGeoIntentsFromMessage(mXmppConnectionService, message);
1277 for (final Intent intent : intents) {
1278 if (intent.resolveActivity(mXmppConnectionService.getPackageManager()) != null) {
1279 return PendingIntent.getActivity(
1280 mXmppConnectionService,
1281 generateRequestCode(message.getConversation(), 18),
1282 intent,
1283 PendingIntent.FLAG_UPDATE_CURRENT);
1284 }
1285 }
1286 return null;
1287 }
1288
1289 private PendingIntent createContentIntent(
1290 final String conversationUuid, final String downloadMessageUuid) {
1291 final Intent viewConversationIntent =
1292 new Intent(mXmppConnectionService, ConversationsActivity.class);
1293 viewConversationIntent.setAction(ConversationsActivity.ACTION_VIEW_CONVERSATION);
1294 viewConversationIntent.putExtra(ConversationsActivity.EXTRA_CONVERSATION, conversationUuid);
1295 if (downloadMessageUuid != null) {
1296 viewConversationIntent.putExtra(
1297 ConversationsActivity.EXTRA_DOWNLOAD_UUID, downloadMessageUuid);
1298 return PendingIntent.getActivity(
1299 mXmppConnectionService,
1300 generateRequestCode(conversationUuid, 8),
1301 viewConversationIntent,
1302 PendingIntent.FLAG_UPDATE_CURRENT);
1303 } else {
1304 return PendingIntent.getActivity(
1305 mXmppConnectionService,
1306 generateRequestCode(conversationUuid, 10),
1307 viewConversationIntent,
1308 PendingIntent.FLAG_UPDATE_CURRENT);
1309 }
1310 }
1311
1312 private int generateRequestCode(String uuid, int actionId) {
1313 return (actionId * NOTIFICATION_ID_MULTIPLIER)
1314 + (uuid.hashCode() % NOTIFICATION_ID_MULTIPLIER);
1315 }
1316
1317 private int generateRequestCode(Conversational conversation, int actionId) {
1318 return generateRequestCode(conversation.getUuid(), actionId);
1319 }
1320
1321 private PendingIntent createDownloadIntent(final Message message) {
1322 return createContentIntent(message.getConversationUuid(), message.getUuid());
1323 }
1324
1325 private PendingIntent createContentIntent(final Conversational conversation) {
1326 return createContentIntent(conversation.getUuid(), null);
1327 }
1328
1329 private PendingIntent createDeleteIntent(Conversation conversation) {
1330 final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
1331 intent.setAction(XmppConnectionService.ACTION_CLEAR_NOTIFICATION);
1332 if (conversation != null) {
1333 intent.putExtra("uuid", conversation.getUuid());
1334 return PendingIntent.getService(
1335 mXmppConnectionService, generateRequestCode(conversation, 20), intent, 0);
1336 }
1337 return PendingIntent.getService(mXmppConnectionService, 0, intent, 0);
1338 }
1339
1340 private PendingIntent createReplyIntent(
1341 final Conversation conversation,
1342 final String lastMessageUuid,
1343 final boolean dismissAfterReply) {
1344 final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
1345 intent.setAction(XmppConnectionService.ACTION_REPLY_TO_CONVERSATION);
1346 intent.putExtra("uuid", conversation.getUuid());
1347 intent.putExtra("dismiss_notification", dismissAfterReply);
1348 intent.putExtra("last_message_uuid", lastMessageUuid);
1349 final int id = generateRequestCode(conversation, dismissAfterReply ? 12 : 14);
1350 return PendingIntent.getService(
1351 mXmppConnectionService, id, intent, PendingIntent.FLAG_UPDATE_CURRENT);
1352 }
1353
1354 private PendingIntent createReadPendingIntent(Conversation conversation) {
1355 final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
1356 intent.setAction(XmppConnectionService.ACTION_MARK_AS_READ);
1357 intent.putExtra("uuid", conversation.getUuid());
1358 intent.setPackage(mXmppConnectionService.getPackageName());
1359 return PendingIntent.getService(
1360 mXmppConnectionService,
1361 generateRequestCode(conversation, 16),
1362 intent,
1363 PendingIntent.FLAG_UPDATE_CURRENT);
1364 }
1365
1366 private PendingIntent createCallAction(String sessionId, final String action, int requestCode) {
1367 final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
1368 intent.setAction(action);
1369 intent.setPackage(mXmppConnectionService.getPackageName());
1370 intent.putExtra(RtpSessionActivity.EXTRA_SESSION_ID, sessionId);
1371 return PendingIntent.getService(
1372 mXmppConnectionService, requestCode, intent, PendingIntent.FLAG_UPDATE_CURRENT);
1373 }
1374
1375 private PendingIntent createSnoozeIntent(Conversation conversation) {
1376 final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
1377 intent.setAction(XmppConnectionService.ACTION_SNOOZE);
1378 intent.putExtra("uuid", conversation.getUuid());
1379 intent.setPackage(mXmppConnectionService.getPackageName());
1380 return PendingIntent.getService(
1381 mXmppConnectionService,
1382 generateRequestCode(conversation, 22),
1383 intent,
1384 PendingIntent.FLAG_UPDATE_CURRENT);
1385 }
1386
1387 private PendingIntent createTryAgainIntent() {
1388 final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
1389 intent.setAction(XmppConnectionService.ACTION_TRY_AGAIN);
1390 return PendingIntent.getService(mXmppConnectionService, 45, intent, 0);
1391 }
1392
1393 private PendingIntent createDismissErrorIntent() {
1394 final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
1395 intent.setAction(XmppConnectionService.ACTION_DISMISS_ERROR_NOTIFICATIONS);
1396 return PendingIntent.getService(mXmppConnectionService, 69, intent, 0);
1397 }
1398
1399 private boolean wasHighlightedOrPrivate(final Message message) {
1400 if (message.getConversation() instanceof Conversation) {
1401 Conversation conversation = (Conversation) message.getConversation();
1402 final String nick = conversation.getMucOptions().getActualNick();
1403 final Pattern highlight = generateNickHighlightPattern(nick);
1404 if (message.getBody() == null || nick == null) {
1405 return false;
1406 }
1407 final Matcher m = highlight.matcher(message.getBody());
1408 return (m.find() || message.isPrivateMessage());
1409 } else {
1410 return false;
1411 }
1412 }
1413
1414 public void setOpenConversation(final Conversation conversation) {
1415 this.mOpenConversation = conversation;
1416 }
1417
1418 public void setIsInForeground(final boolean foreground) {
1419 this.mIsInForeground = foreground;
1420 }
1421
1422 private int getPixel(final int dp) {
1423 final DisplayMetrics metrics = mXmppConnectionService.getResources().getDisplayMetrics();
1424 return ((int) (dp * metrics.density));
1425 }
1426
1427 private void markLastNotification() {
1428 this.mLastNotification = SystemClock.elapsedRealtime();
1429 }
1430
1431 private boolean inMiniGracePeriod(final Account account) {
1432 final int miniGrace =
1433 account.getStatus() == Account.State.ONLINE
1434 ? Config.MINI_GRACE_PERIOD
1435 : Config.MINI_GRACE_PERIOD * 2;
1436 return SystemClock.elapsedRealtime() < (this.mLastNotification + miniGrace);
1437 }
1438
1439 Notification createForegroundNotification() {
1440 final Notification.Builder mBuilder = new Notification.Builder(mXmppConnectionService);
1441 mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.app_name));
1442 final List<Account> accounts = mXmppConnectionService.getAccounts();
1443 int enabled = 0;
1444 int connected = 0;
1445 if (accounts != null) {
1446 for (Account account : accounts) {
1447 if (account.isOnlineAndConnected()) {
1448 connected++;
1449 enabled++;
1450 } else if (account.isEnabled()) {
1451 enabled++;
1452 }
1453 }
1454 }
1455 mBuilder.setContentText(
1456 mXmppConnectionService.getString(R.string.connected_accounts, connected, enabled));
1457 final PendingIntent openIntent = createOpenConversationsIntent();
1458 if (openIntent != null) {
1459 mBuilder.setContentIntent(openIntent);
1460 }
1461 mBuilder.setWhen(0)
1462 .setPriority(Notification.PRIORITY_MIN)
1463 .setSmallIcon(
1464 connected > 0
1465 ? R.drawable.ic_link_white_24dp
1466 : R.drawable.ic_link_off_white_24dp)
1467 .setLocalOnly(true);
1468
1469 if (Compatibility.runsTwentySix()) {
1470 mBuilder.setChannelId("foreground");
1471 }
1472
1473 return mBuilder.build();
1474 }
1475
1476 private PendingIntent createOpenConversationsIntent() {
1477 try {
1478 return PendingIntent.getActivity(
1479 mXmppConnectionService,
1480 0,
1481 new Intent(mXmppConnectionService, ConversationsActivity.class),
1482 0);
1483 } catch (RuntimeException e) {
1484 return null;
1485 }
1486 }
1487
1488 void updateErrorNotification() {
1489 if (Config.SUPPRESS_ERROR_NOTIFICATION) {
1490 cancel(ERROR_NOTIFICATION_ID);
1491 return;
1492 }
1493 final boolean showAllErrors = QuickConversationsService.isConversations();
1494 final List<Account> errors = new ArrayList<>();
1495 boolean torNotAvailable = false;
1496 for (final Account account : mXmppConnectionService.getAccounts()) {
1497 if (account.hasErrorStatus()
1498 && account.showErrorNotification()
1499 && (showAllErrors
1500 || account.getLastErrorStatus() == Account.State.UNAUTHORIZED)) {
1501 errors.add(account);
1502 torNotAvailable |= account.getStatus() == Account.State.TOR_NOT_AVAILABLE;
1503 }
1504 }
1505 if (mXmppConnectionService.foregroundNotificationNeedsUpdatingWhenErrorStateChanges()) {
1506 notify(FOREGROUND_NOTIFICATION_ID, createForegroundNotification());
1507 }
1508 final Notification.Builder mBuilder = new Notification.Builder(mXmppConnectionService);
1509 if (errors.size() == 0) {
1510 cancel(ERROR_NOTIFICATION_ID);
1511 return;
1512 } else if (errors.size() == 1) {
1513 mBuilder.setContentTitle(
1514 mXmppConnectionService.getString(R.string.problem_connecting_to_account));
1515 mBuilder.setContentText(errors.get(0).getJid().asBareJid().toEscapedString());
1516 } else {
1517 mBuilder.setContentTitle(
1518 mXmppConnectionService.getString(R.string.problem_connecting_to_accounts));
1519 mBuilder.setContentText(mXmppConnectionService.getString(R.string.touch_to_fix));
1520 }
1521 mBuilder.addAction(
1522 R.drawable.ic_autorenew_white_24dp,
1523 mXmppConnectionService.getString(R.string.try_again),
1524 createTryAgainIntent());
1525 if (torNotAvailable) {
1526 if (TorServiceUtils.isOrbotInstalled(mXmppConnectionService)) {
1527 mBuilder.addAction(
1528 R.drawable.ic_play_circle_filled_white_48dp,
1529 mXmppConnectionService.getString(R.string.start_orbot),
1530 PendingIntent.getActivity(
1531 mXmppConnectionService, 147, TorServiceUtils.LAUNCH_INTENT, 0));
1532 } else {
1533 mBuilder.addAction(
1534 R.drawable.ic_file_download_white_24dp,
1535 mXmppConnectionService.getString(R.string.install_orbot),
1536 PendingIntent.getActivity(
1537 mXmppConnectionService, 146, TorServiceUtils.INSTALL_INTENT, 0));
1538 }
1539 }
1540 mBuilder.setDeleteIntent(createDismissErrorIntent());
1541 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
1542 mBuilder.setVisibility(Notification.VISIBILITY_PRIVATE);
1543 mBuilder.setSmallIcon(R.drawable.ic_warning_white_24dp);
1544 } else {
1545 mBuilder.setSmallIcon(R.drawable.ic_stat_alert_warning);
1546 }
1547 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) {
1548 mBuilder.setLocalOnly(true);
1549 }
1550 mBuilder.setPriority(Notification.PRIORITY_LOW);
1551 final Intent intent;
1552 if (AccountUtils.MANAGE_ACCOUNT_ACTIVITY != null) {
1553 intent = new Intent(mXmppConnectionService, AccountUtils.MANAGE_ACCOUNT_ACTIVITY);
1554 } else {
1555 intent = new Intent(mXmppConnectionService, EditAccountActivity.class);
1556 intent.putExtra("jid", errors.get(0).getJid().asBareJid().toEscapedString());
1557 intent.putExtra(EditAccountActivity.EXTRA_OPENED_FROM_NOTIFICATION, true);
1558 }
1559 mBuilder.setContentIntent(
1560 PendingIntent.getActivity(
1561 mXmppConnectionService, 145, intent, PendingIntent.FLAG_UPDATE_CURRENT));
1562 if (Compatibility.runsTwentySix()) {
1563 mBuilder.setChannelId("error");
1564 }
1565 notify(ERROR_NOTIFICATION_ID, mBuilder.build());
1566 }
1567
1568 void updateFileAddingNotification(int current, Message message) {
1569 Notification.Builder mBuilder = new Notification.Builder(mXmppConnectionService);
1570 mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.transcoding_video));
1571 mBuilder.setProgress(100, current, false);
1572 mBuilder.setSmallIcon(R.drawable.ic_hourglass_empty_white_24dp);
1573 mBuilder.setContentIntent(createContentIntent(message.getConversation()));
1574 mBuilder.setOngoing(true);
1575 if (Compatibility.runsTwentySix()) {
1576 mBuilder.setChannelId("compression");
1577 }
1578 Notification notification = mBuilder.build();
1579 notify(FOREGROUND_NOTIFICATION_ID, notification);
1580 }
1581
1582 private void notify(String tag, int id, Notification notification) {
1583 final NotificationManagerCompat notificationManager =
1584 NotificationManagerCompat.from(mXmppConnectionService);
1585 try {
1586 notificationManager.notify(tag, id, notification);
1587 } catch (RuntimeException e) {
1588 Log.d(Config.LOGTAG, "unable to make notification", e);
1589 }
1590 }
1591
1592 public void notify(int id, Notification notification) {
1593 final NotificationManagerCompat notificationManager =
1594 NotificationManagerCompat.from(mXmppConnectionService);
1595 try {
1596 notificationManager.notify(id, notification);
1597 } catch (RuntimeException e) {
1598 Log.d(Config.LOGTAG, "unable to make notification", e);
1599 }
1600 }
1601
1602 public void cancel(int id) {
1603 final NotificationManagerCompat notificationManager =
1604 NotificationManagerCompat.from(mXmppConnectionService);
1605 try {
1606 notificationManager.cancel(id);
1607 } catch (RuntimeException e) {
1608 Log.d(Config.LOGTAG, "unable to cancel notification", e);
1609 }
1610 }
1611
1612 private void cancel(String tag, int id) {
1613 final NotificationManagerCompat notificationManager =
1614 NotificationManagerCompat.from(mXmppConnectionService);
1615 try {
1616 notificationManager.cancel(tag, id);
1617 } catch (RuntimeException e) {
1618 Log.d(Config.LOGTAG, "unable to cancel notification", e);
1619 }
1620 }
1621
1622 private class VibrationRunnable implements Runnable {
1623
1624 @Override
1625 public void run() {
1626 final Vibrator vibrator =
1627 (Vibrator) mXmppConnectionService.getSystemService(Context.VIBRATOR_SERVICE);
1628 vibrator.vibrate(CALL_PATTERN, -1);
1629 }
1630 }
1631}