1package eu.siacs.conversations.services;
2
3import static eu.siacs.conversations.utils.Compatibility.s;
4
5import android.Manifest;
6import android.app.Notification;
7import android.app.NotificationChannel;
8import android.app.NotificationChannelGroup;
9import android.app.NotificationManager;
10import android.app.PendingIntent;
11import android.content.Context;
12import android.content.Intent;
13import android.content.SharedPreferences;
14import android.content.pm.PackageManager;
15import android.content.pm.ShortcutManager;
16import android.content.res.Resources;
17import android.graphics.Bitmap;
18import android.graphics.Typeface;
19import android.media.AudioAttributes;
20import android.media.AudioManager;
21import android.media.RingtoneManager;
22import android.net.Uri;
23import android.os.Build;
24import android.os.SystemClock;
25import android.preference.PreferenceManager;
26import android.provider.Settings;
27import android.text.SpannableString;
28import android.text.style.StyleSpan;
29import android.util.DisplayMetrics;
30import android.util.Log;
31import androidx.annotation.Nullable;
32import androidx.annotation.RequiresApi;
33import androidx.core.app.ActivityCompat;
34import androidx.core.app.NotificationCompat;
35import androidx.core.app.NotificationCompat.BigPictureStyle;
36import androidx.core.app.NotificationCompat.Builder;
37import androidx.core.app.NotificationManagerCompat;
38import androidx.core.app.Person;
39import androidx.core.app.RemoteInput;
40import androidx.core.content.ContextCompat;
41import androidx.core.content.pm.ShortcutInfoCompat;
42import androidx.core.graphics.drawable.IconCompat;
43import com.google.common.base.Joiner;
44import com.google.common.base.Optional;
45import com.google.common.base.Splitter;
46import com.google.common.base.Strings;
47import com.google.common.collect.ImmutableMap;
48import com.google.common.collect.Iterables;
49import com.google.common.primitives.Ints;
50import eu.siacs.conversations.AppSettings;
51import eu.siacs.conversations.Config;
52import eu.siacs.conversations.R;
53import eu.siacs.conversations.entities.Account;
54import eu.siacs.conversations.entities.Contact;
55import eu.siacs.conversations.entities.Conversation;
56import eu.siacs.conversations.entities.Conversational;
57import eu.siacs.conversations.entities.Message;
58import eu.siacs.conversations.persistance.FileBackend;
59import eu.siacs.conversations.ui.ConversationsActivity;
60import eu.siacs.conversations.ui.EditAccountActivity;
61import eu.siacs.conversations.ui.RtpSessionActivity;
62import eu.siacs.conversations.utils.AccountUtils;
63import eu.siacs.conversations.utils.Compatibility;
64import eu.siacs.conversations.utils.GeoHelper;
65import eu.siacs.conversations.utils.TorServiceUtils;
66import eu.siacs.conversations.utils.UIHelper;
67import eu.siacs.conversations.xmpp.XmppConnection;
68import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection;
69import eu.siacs.conversations.xmpp.jingle.Media;
70import java.io.File;
71import java.io.IOException;
72import java.util.ArrayList;
73import java.util.Arrays;
74import java.util.Collections;
75import java.util.HashMap;
76import java.util.Iterator;
77import java.util.LinkedHashMap;
78import java.util.List;
79import java.util.Map;
80import java.util.Set;
81import java.util.UUID;
82import java.util.concurrent.atomic.AtomicInteger;
83import java.util.regex.Matcher;
84import java.util.regex.Pattern;
85
86public class NotificationService {
87
88 public static final Object CATCHUP_LOCK = new Object();
89
90 private static final int LED_COLOR = 0xff00ff00;
91
92 private static final long[] CALL_PATTERN = {0, 500, 300, 600, 3000};
93
94 private static final String MESSAGES_GROUP = "eu.siacs.conversations.messages";
95 private static final String MISSED_CALLS_GROUP = "eu.siacs.conversations.missed_calls";
96 private static final int NOTIFICATION_ID_MULTIPLIER = 1024 * 1024;
97 static final int FOREGROUND_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 4;
98 private static final int NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 2;
99 private static final int ERROR_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 6;
100 private static final int INCOMING_CALL_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 8;
101 public static final int ONGOING_CALL_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 10;
102 public static final int MISSED_CALL_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 12;
103 private static final int DELIVERY_FAILED_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 13;
104 public static final int ONGOING_VIDEO_TRANSCODING_NOTIFICATION_ID =
105 NOTIFICATION_ID_MULTIPLIER * 14;
106 private final XmppConnectionService mXmppConnectionService;
107 private final LinkedHashMap<String, ArrayList<Message>> notifications = new LinkedHashMap<>();
108 private final HashMap<Conversation, AtomicInteger> mBacklogMessageCounter = new HashMap<>();
109 private final LinkedHashMap<Conversational, MissedCallsInfo> mMissedCalls =
110 new LinkedHashMap<>();
111 private Conversation mOpenConversation;
112 private boolean mIsInForeground;
113 private long mLastNotification;
114
115 private static final String INCOMING_CALLS_NOTIFICATION_CHANNEL = "incoming_calls_channel";
116 private static final String INCOMING_CALLS_NOTIFICATION_CHANNEL_PREFIX =
117 "incoming_calls_channel#";
118 public static final String MESSAGES_NOTIFICATION_CHANNEL = "messages";
119
120 NotificationService(final XmppConnectionService service) {
121 this.mXmppConnectionService = service;
122 }
123
124 private static boolean displaySnoozeAction(List<Message> messages) {
125 int numberOfMessagesWithoutReply = 0;
126 for (Message message : messages) {
127 if (message.getStatus() == Message.STATUS_RECEIVED) {
128 ++numberOfMessagesWithoutReply;
129 } else {
130 return false;
131 }
132 }
133 return numberOfMessagesWithoutReply >= 3;
134 }
135
136 public static Pattern generateNickHighlightPattern(final String nick) {
137 return Pattern.compile("(?<=(^|\\s))" + Pattern.quote(nick) + "(?=\\s|$|\\p{Punct})");
138 }
139
140 private static boolean isImageMessage(Message message) {
141 return message.getType() != Message.TYPE_TEXT
142 && message.getTransferable() == null
143 && !message.isDeleted()
144 && message.getEncryption() != Message.ENCRYPTION_PGP
145 && message.getFileParams().height > 0;
146 }
147
148 @RequiresApi(api = Build.VERSION_CODES.O)
149 void initializeChannels() {
150 final Context c = mXmppConnectionService;
151 final NotificationManager notificationManager =
152 c.getSystemService(NotificationManager.class);
153 if (notificationManager == null) {
154 return;
155 }
156
157 notificationManager.deleteNotificationChannel("export");
158 notificationManager.deleteNotificationChannel("incoming_calls");
159 notificationManager.deleteNotificationChannel(INCOMING_CALLS_NOTIFICATION_CHANNEL);
160
161 notificationManager.createNotificationChannelGroup(
162 new NotificationChannelGroup(
163 "status", c.getString(R.string.notification_group_status_information)));
164 notificationManager.createNotificationChannelGroup(
165 new NotificationChannelGroup(
166 "chats", c.getString(R.string.notification_group_messages)));
167 notificationManager.createNotificationChannelGroup(
168 new NotificationChannelGroup(
169 "calls", c.getString(R.string.notification_group_calls)));
170 final NotificationChannel foregroundServiceChannel =
171 new NotificationChannel(
172 "foreground",
173 c.getString(R.string.foreground_service_channel_name),
174 NotificationManager.IMPORTANCE_MIN);
175 foregroundServiceChannel.setDescription(
176 c.getString(
177 R.string.foreground_service_channel_description,
178 c.getString(R.string.app_name)));
179 foregroundServiceChannel.setShowBadge(false);
180 foregroundServiceChannel.setGroup("status");
181 notificationManager.createNotificationChannel(foregroundServiceChannel);
182 final NotificationChannel errorChannel =
183 new NotificationChannel(
184 "error",
185 c.getString(R.string.error_channel_name),
186 NotificationManager.IMPORTANCE_LOW);
187 errorChannel.setDescription(c.getString(R.string.error_channel_description));
188 errorChannel.setShowBadge(false);
189 errorChannel.setGroup("status");
190 notificationManager.createNotificationChannel(errorChannel);
191
192 final NotificationChannel videoCompressionChannel =
193 new NotificationChannel(
194 "compression",
195 c.getString(R.string.video_compression_channel_name),
196 NotificationManager.IMPORTANCE_LOW);
197 videoCompressionChannel.setShowBadge(false);
198 videoCompressionChannel.setGroup("status");
199 notificationManager.createNotificationChannel(videoCompressionChannel);
200
201 final NotificationChannel exportChannel =
202 new NotificationChannel(
203 "backup",
204 c.getString(R.string.backup_channel_name),
205 NotificationManager.IMPORTANCE_LOW);
206 exportChannel.setShowBadge(false);
207 exportChannel.setGroup("status");
208 notificationManager.createNotificationChannel(exportChannel);
209
210 createInitialIncomingCallChannelIfNecessary(c);
211
212 final NotificationChannel ongoingCallsChannel =
213 new NotificationChannel(
214 "ongoing_calls",
215 c.getString(R.string.ongoing_calls_channel_name),
216 NotificationManager.IMPORTANCE_LOW);
217 ongoingCallsChannel.setShowBadge(false);
218 ongoingCallsChannel.setGroup("calls");
219 notificationManager.createNotificationChannel(ongoingCallsChannel);
220
221 final NotificationChannel missedCallsChannel =
222 new NotificationChannel(
223 "missed_calls",
224 c.getString(R.string.missed_calls_channel_name),
225 NotificationManager.IMPORTANCE_HIGH);
226 missedCallsChannel.setShowBadge(true);
227 missedCallsChannel.setSound(null, null);
228 missedCallsChannel.setLightColor(LED_COLOR);
229 missedCallsChannel.enableLights(true);
230 missedCallsChannel.setGroup("calls");
231 notificationManager.createNotificationChannel(missedCallsChannel);
232
233 final var messagesChannel =
234 prepareMessagesChannel(mXmppConnectionService, MESSAGES_NOTIFICATION_CHANNEL);
235 notificationManager.createNotificationChannel(messagesChannel);
236 final NotificationChannel silentMessagesChannel =
237 new NotificationChannel(
238 "silent_messages",
239 c.getString(R.string.silent_messages_channel_name),
240 NotificationManager.IMPORTANCE_LOW);
241 silentMessagesChannel.setDescription(
242 c.getString(R.string.silent_messages_channel_description));
243 silentMessagesChannel.setShowBadge(true);
244 silentMessagesChannel.setLightColor(LED_COLOR);
245 silentMessagesChannel.enableLights(true);
246 silentMessagesChannel.setGroup("chats");
247 notificationManager.createNotificationChannel(silentMessagesChannel);
248
249 final NotificationChannel deliveryFailedChannel =
250 new NotificationChannel(
251 "delivery_failed",
252 c.getString(R.string.delivery_failed_channel_name),
253 NotificationManager.IMPORTANCE_DEFAULT);
254 deliveryFailedChannel.setShowBadge(false);
255 deliveryFailedChannel.setSound(
256 RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION),
257 new AudioAttributes.Builder()
258 .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
259 .setUsage(AudioAttributes.USAGE_NOTIFICATION)
260 .build());
261 deliveryFailedChannel.setGroup("chats");
262 notificationManager.createNotificationChannel(deliveryFailedChannel);
263 }
264
265 @RequiresApi(api = Build.VERSION_CODES.R)
266 public static void createConversationChannel(
267 final Context context, final ShortcutInfoCompat shortcut) {
268 final var messagesChannel = prepareMessagesChannel(context, UUID.randomUUID().toString());
269 messagesChannel.setName(shortcut.getShortLabel());
270 messagesChannel.setConversationId(MESSAGES_NOTIFICATION_CHANNEL, shortcut.getId());
271 final var notificationManager = context.getSystemService(NotificationManager.class);
272 notificationManager.createNotificationChannel(messagesChannel);
273 }
274
275 @RequiresApi(api = Build.VERSION_CODES.O)
276 private static NotificationChannel prepareMessagesChannel(
277 final Context context, final String id) {
278 final NotificationChannel messagesChannel =
279 new NotificationChannel(
280 id,
281 context.getString(R.string.messages_channel_name),
282 NotificationManager.IMPORTANCE_HIGH);
283 messagesChannel.setShowBadge(true);
284 messagesChannel.setSound(
285 RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION),
286 new AudioAttributes.Builder()
287 .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
288 .setUsage(AudioAttributes.USAGE_NOTIFICATION)
289 .build());
290 messagesChannel.setLightColor(LED_COLOR);
291 final int dat = 70;
292 final long[] pattern = {0, 3 * dat, dat, dat};
293 messagesChannel.setVibrationPattern(pattern);
294 messagesChannel.enableVibration(true);
295 messagesChannel.enableLights(true);
296 messagesChannel.setGroup("chats");
297 return messagesChannel;
298 }
299
300 @RequiresApi(api = Build.VERSION_CODES.O)
301 private static void createInitialIncomingCallChannelIfNecessary(final Context context) {
302 final var currentIteration = getCurrentIncomingCallChannelIteration(context);
303 if (currentIteration.isPresent()) {
304 return;
305 }
306 createInitialIncomingCallChannel(context);
307 }
308
309 @RequiresApi(api = Build.VERSION_CODES.O)
310 public static Optional<Integer> getCurrentIncomingCallChannelIteration(final Context context) {
311 final var notificationManager = context.getSystemService(NotificationManager.class);
312 for (final NotificationChannel channel : notificationManager.getNotificationChannels()) {
313 final String id = channel.getId();
314 if (Strings.isNullOrEmpty(id)) {
315 continue;
316 }
317 if (id.startsWith(INCOMING_CALLS_NOTIFICATION_CHANNEL_PREFIX)) {
318 final var parts = Splitter.on('#').splitToList(id);
319 if (parts.size() == 2) {
320 final var iteration = Ints.tryParse(parts.get(1));
321 if (iteration != null) {
322 return Optional.of(iteration);
323 }
324 }
325 }
326 }
327 return Optional.absent();
328 }
329
330 @RequiresApi(api = Build.VERSION_CODES.O)
331 public static Optional<NotificationChannel> getCurrentIncomingCallChannel(
332 final Context context) {
333 final var iteration = getCurrentIncomingCallChannelIteration(context);
334 return iteration.transform(
335 i -> {
336 final var notificationManager =
337 context.getSystemService(NotificationManager.class);
338 return notificationManager.getNotificationChannel(
339 INCOMING_CALLS_NOTIFICATION_CHANNEL_PREFIX + i);
340 });
341 }
342
343 @RequiresApi(api = Build.VERSION_CODES.O)
344 private static void createInitialIncomingCallChannel(final Context context) {
345 final var appSettings = new AppSettings(context);
346 final var ringtoneUri = appSettings.getRingtone();
347 createIncomingCallChannel(context, ringtoneUri, 0);
348 }
349
350 @RequiresApi(api = Build.VERSION_CODES.O)
351 public static void recreateIncomingCallChannel(final Context context, final Uri ringtone) {
352 final var currentIteration = getCurrentIncomingCallChannelIteration(context);
353 final int nextIteration;
354 if (currentIteration.isPresent()) {
355 final var notificationManager = context.getSystemService(NotificationManager.class);
356 notificationManager.deleteNotificationChannel(
357 INCOMING_CALLS_NOTIFICATION_CHANNEL_PREFIX + currentIteration.get());
358 nextIteration = currentIteration.get() + 1;
359 } else {
360 nextIteration = 0;
361 }
362 createIncomingCallChannel(context, ringtone, nextIteration);
363 }
364
365 @RequiresApi(api = Build.VERSION_CODES.O)
366 private static void createIncomingCallChannel(
367 final Context context, final Uri ringtoneUri, final int iteration) {
368 final var notificationManager = context.getSystemService(NotificationManager.class);
369 final var id = INCOMING_CALLS_NOTIFICATION_CHANNEL_PREFIX + iteration;
370 Log.d(Config.LOGTAG, "creating incoming call channel with id " + id);
371 final NotificationChannel incomingCallsChannel =
372 new NotificationChannel(
373 id,
374 context.getString(R.string.incoming_calls_channel_name),
375 NotificationManager.IMPORTANCE_HIGH);
376 incomingCallsChannel.setSound(
377 ringtoneUri,
378 new AudioAttributes.Builder()
379 .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
380 .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
381 .build());
382 incomingCallsChannel.setShowBadge(false);
383 incomingCallsChannel.setLightColor(LED_COLOR);
384 incomingCallsChannel.enableLights(true);
385 incomingCallsChannel.setGroup("calls");
386 incomingCallsChannel.setBypassDnd(true);
387 incomingCallsChannel.enableVibration(true);
388 incomingCallsChannel.setVibrationPattern(CALL_PATTERN);
389 notificationManager.createNotificationChannel(incomingCallsChannel);
390 }
391
392 private boolean notifyMessage(final Message message) {
393 final Conversation conversation = (Conversation) message.getConversation();
394 return message.getStatus() == Message.STATUS_RECEIVED
395 && !conversation.isMuted()
396 && (conversation.alwaysNotify() || wasHighlightedOrPrivate(message))
397 && (!conversation.isWithStranger() || notificationsFromStrangers())
398 && message.getType() != Message.TYPE_RTP_SESSION;
399 }
400
401 private boolean notifyMissedCall(final Message message) {
402 return message.getType() == Message.TYPE_RTP_SESSION
403 && message.getStatus() == Message.STATUS_RECEIVED;
404 }
405
406 public boolean notificationsFromStrangers() {
407 return mXmppConnectionService.getBooleanPreference(
408 "notifications_from_strangers", R.bool.notifications_from_strangers);
409 }
410
411 public void pushFromBacklog(final Message message) {
412 if (notifyMessage(message)) {
413 synchronized (notifications) {
414 getBacklogMessageCounter((Conversation) message.getConversation())
415 .incrementAndGet();
416 pushToStack(message);
417 }
418 } else if (notifyMissedCall(message)) {
419 synchronized (mMissedCalls) {
420 pushMissedCall(message);
421 }
422 }
423 }
424
425 private AtomicInteger getBacklogMessageCounter(Conversation conversation) {
426 synchronized (mBacklogMessageCounter) {
427 if (!mBacklogMessageCounter.containsKey(conversation)) {
428 mBacklogMessageCounter.put(conversation, new AtomicInteger(0));
429 }
430 return mBacklogMessageCounter.get(conversation);
431 }
432 }
433
434 void pushFromDirectReply(final Message message) {
435 synchronized (notifications) {
436 pushToStack(message);
437 updateNotification(false);
438 }
439 }
440
441 public void finishBacklog(boolean notify, Account account) {
442 synchronized (notifications) {
443 mXmppConnectionService.updateUnreadCountBadge();
444 if (account == null || !notify) {
445 updateNotification(notify);
446 } else {
447 final int count;
448 final List<String> conversations;
449 synchronized (this.mBacklogMessageCounter) {
450 conversations = getBacklogConversations(account);
451 count = getBacklogMessageCount(account);
452 }
453 updateNotification(count > 0, conversations);
454 }
455 }
456 synchronized (mMissedCalls) {
457 updateMissedCallNotifications(mMissedCalls.keySet());
458 }
459 }
460
461 private List<String> getBacklogConversations(Account account) {
462 final List<String> conversations = new ArrayList<>();
463 for (Map.Entry<Conversation, AtomicInteger> entry : mBacklogMessageCounter.entrySet()) {
464 if (entry.getKey().getAccount() == account) {
465 conversations.add(entry.getKey().getUuid());
466 }
467 }
468 return conversations;
469 }
470
471 private int getBacklogMessageCount(Account account) {
472 int count = 0;
473 for (Iterator<Map.Entry<Conversation, AtomicInteger>> it =
474 mBacklogMessageCounter.entrySet().iterator();
475 it.hasNext(); ) {
476 Map.Entry<Conversation, AtomicInteger> entry = it.next();
477 if (entry.getKey().getAccount() == account) {
478 count += entry.getValue().get();
479 it.remove();
480 }
481 }
482 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": backlog message count=" + count);
483 return count;
484 }
485
486 void finishBacklog() {
487 finishBacklog(false, null);
488 }
489
490 private void pushToStack(final Message message) {
491 final String conversationUuid = message.getConversationUuid();
492 if (notifications.containsKey(conversationUuid)) {
493 notifications.get(conversationUuid).add(message);
494 } else {
495 final ArrayList<Message> mList = new ArrayList<>();
496 mList.add(message);
497 notifications.put(conversationUuid, mList);
498 }
499 }
500
501 public void push(final Message message) {
502 synchronized (CATCHUP_LOCK) {
503 final XmppConnection connection =
504 message.getConversation().getAccount().getXmppConnection();
505 if (connection != null && connection.isWaitingForSmCatchup()) {
506 connection.incrementSmCatchupMessageCounter();
507 pushFromBacklog(message);
508 } else {
509 pushNow(message);
510 }
511 }
512 }
513
514 public void pushFailedDelivery(final Message message) {
515 final Conversation conversation = (Conversation) message.getConversation();
516 final boolean isScreenLocked = !mXmppConnectionService.isScreenLocked();
517 if (this.mIsInForeground
518 && isScreenLocked
519 && this.mOpenConversation == message.getConversation()) {
520 Log.d(
521 Config.LOGTAG,
522 message.getConversation().getAccount().getJid().asBareJid()
523 + ": suppressing failed delivery notification because conversation is"
524 + " open");
525 return;
526 }
527 final PendingIntent pendingIntent = createContentIntent(conversation);
528 final int notificationId =
529 generateRequestCode(conversation, 0) + DELIVERY_FAILED_NOTIFICATION_ID;
530 final int failedDeliveries = conversation.countFailedDeliveries();
531 final Notification notification =
532 new Builder(mXmppConnectionService, "delivery_failed")
533 .setContentTitle(conversation.getName())
534 .setAutoCancel(true)
535 .setSmallIcon(R.drawable.ic_error_24dp)
536 .setContentText(
537 mXmppConnectionService
538 .getResources()
539 .getQuantityText(
540 R.plurals.some_messages_could_not_be_delivered,
541 failedDeliveries))
542 .setGroup("delivery_failed")
543 .setContentIntent(pendingIntent)
544 .build();
545 final Notification summaryNotification =
546 new Builder(mXmppConnectionService, "delivery_failed")
547 .setContentTitle(
548 mXmppConnectionService.getString(R.string.failed_deliveries))
549 .setContentText(
550 mXmppConnectionService
551 .getResources()
552 .getQuantityText(
553 R.plurals.some_messages_could_not_be_delivered,
554 1024))
555 .setSmallIcon(R.drawable.ic_error_24dp)
556 .setGroup("delivery_failed")
557 .setGroupSummary(true)
558 .setAutoCancel(true)
559 .build();
560 notify(notificationId, notification);
561 notify(DELIVERY_FAILED_NOTIFICATION_ID, summaryNotification);
562 }
563
564 public synchronized void startRinging(
565 final AbstractJingleConnection.Id id, final Set<Media> media) {
566 showIncomingCallNotification(id, media, false);
567 }
568
569 private void showIncomingCallNotification(
570 final AbstractJingleConnection.Id id,
571 final Set<Media> media,
572 final boolean onlyAlertOnce) {
573 final Intent fullScreenIntent =
574 new Intent(mXmppConnectionService, RtpSessionActivity.class);
575 fullScreenIntent.putExtra(
576 RtpSessionActivity.EXTRA_ACCOUNT,
577 id.account.getJid().asBareJid().toEscapedString());
578 fullScreenIntent.putExtra(RtpSessionActivity.EXTRA_WITH, id.with.toEscapedString());
579 fullScreenIntent.putExtra(RtpSessionActivity.EXTRA_SESSION_ID, id.sessionId);
580 fullScreenIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
581 fullScreenIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
582 final int channelIteration;
583 if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
584 channelIteration = getCurrentIncomingCallChannelIteration(mXmppConnectionService).or(0);
585 } else {
586 channelIteration = 0;
587 }
588 final var channelId = INCOMING_CALLS_NOTIFICATION_CHANNEL_PREFIX + channelIteration;
589 Log.d(
590 Config.LOGTAG,
591 "showing incoming call notification on channel "
592 + channelId
593 + ", onlyAlertOnce="
594 + onlyAlertOnce);
595 final NotificationCompat.Builder builder =
596 new NotificationCompat.Builder(mXmppConnectionService, channelId);
597 if (media.contains(Media.VIDEO)) {
598 builder.setSmallIcon(R.drawable.ic_videocam_24dp);
599 builder.setContentTitle(
600 mXmppConnectionService.getString(R.string.rtp_state_incoming_video_call));
601 } else {
602 builder.setSmallIcon(R.drawable.ic_call_24dp);
603 builder.setContentTitle(
604 mXmppConnectionService.getString(R.string.rtp_state_incoming_call));
605 }
606 final Contact contact = id.getContact();
607 builder.setLargeIcon(
608 mXmppConnectionService
609 .getAvatarService()
610 .get(contact, AvatarService.getSystemUiAvatarSize(mXmppConnectionService)));
611 final Uri systemAccount = contact.getSystemAccount();
612 if (systemAccount != null) {
613 builder.addPerson(systemAccount.toString());
614 }
615 if (!onlyAlertOnce) {
616 final var appSettings = new AppSettings(mXmppConnectionService);
617 final var ringtone = appSettings.getRingtone();
618 if (ringtone != null) {
619 builder.setSound(ringtone, AudioManager.STREAM_RING);
620 }
621 builder.setVibrate(CALL_PATTERN);
622 }
623 builder.setOnlyAlertOnce(onlyAlertOnce);
624 builder.setContentText(id.account.getRoster().getContact(id.with).getDisplayName());
625 builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC);
626 builder.setPriority(NotificationCompat.PRIORITY_HIGH);
627 builder.setCategory(NotificationCompat.CATEGORY_CALL);
628 final PendingIntent pendingIntent = createPendingRtpSession(id, Intent.ACTION_VIEW, 101);
629 builder.setFullScreenIntent(pendingIntent, true);
630 builder.setContentIntent(pendingIntent); // old androids need this?
631 builder.setOngoing(true);
632 builder.addAction(
633 new NotificationCompat.Action.Builder(
634 R.drawable.ic_call_end_24dp,
635 mXmppConnectionService.getString(R.string.dismiss_call),
636 createCallAction(
637 id.sessionId,
638 XmppConnectionService.ACTION_DISMISS_CALL,
639 102))
640 .build());
641 builder.addAction(
642 new NotificationCompat.Action.Builder(
643 R.drawable.ic_call_24dp,
644 mXmppConnectionService.getString(R.string.answer_call),
645 createPendingRtpSession(
646 id, RtpSessionActivity.ACTION_ACCEPT_CALL, 103))
647 .build());
648 modifyIncomingCall(builder);
649 final Notification notification = builder.build();
650 notification.audioAttributes =
651 new AudioAttributes.Builder()
652 .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
653 .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
654 .build();
655 notification.flags = notification.flags | Notification.FLAG_INSISTENT;
656 notify(INCOMING_CALL_NOTIFICATION_ID, notification);
657 }
658
659 public Notification getOngoingCallNotification(
660 final XmppConnectionService.OngoingCall ongoingCall) {
661 final AbstractJingleConnection.Id id = ongoingCall.id;
662 final NotificationCompat.Builder builder =
663 new NotificationCompat.Builder(mXmppConnectionService, "ongoing_calls");
664 if (ongoingCall.media.contains(Media.VIDEO)) {
665 builder.setSmallIcon(R.drawable.ic_videocam_24dp);
666 if (ongoingCall.reconnecting) {
667 builder.setContentTitle(
668 mXmppConnectionService.getString(R.string.reconnecting_video_call));
669 } else {
670 builder.setContentTitle(
671 mXmppConnectionService.getString(R.string.ongoing_video_call));
672 }
673 } else {
674 builder.setSmallIcon(R.drawable.ic_call_24dp);
675 if (ongoingCall.reconnecting) {
676 builder.setContentTitle(
677 mXmppConnectionService.getString(R.string.reconnecting_call));
678 } else {
679 builder.setContentTitle(mXmppConnectionService.getString(R.string.ongoing_call));
680 }
681 }
682 builder.setContentText(id.account.getRoster().getContact(id.with).getDisplayName());
683 builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC);
684 builder.setPriority(NotificationCompat.PRIORITY_HIGH);
685 builder.setCategory(NotificationCompat.CATEGORY_CALL);
686 builder.setContentIntent(createPendingRtpSession(id, Intent.ACTION_VIEW, 101));
687 builder.setOngoing(true);
688 builder.addAction(
689 new NotificationCompat.Action.Builder(
690 R.drawable.ic_call_end_24dp,
691 mXmppConnectionService.getString(R.string.hang_up),
692 createCallAction(
693 id.sessionId, XmppConnectionService.ACTION_END_CALL, 104))
694 .build());
695 builder.setLocalOnly(true);
696 return builder.build();
697 }
698
699 private PendingIntent createPendingRtpSession(
700 final AbstractJingleConnection.Id id, final String action, final int requestCode) {
701 final Intent fullScreenIntent =
702 new Intent(mXmppConnectionService, RtpSessionActivity.class);
703 fullScreenIntent.setAction(action);
704 fullScreenIntent.putExtra(
705 RtpSessionActivity.EXTRA_ACCOUNT,
706 id.account.getJid().asBareJid().toEscapedString());
707 fullScreenIntent.putExtra(RtpSessionActivity.EXTRA_WITH, id.with.toEscapedString());
708 fullScreenIntent.putExtra(RtpSessionActivity.EXTRA_SESSION_ID, id.sessionId);
709 return PendingIntent.getActivity(
710 mXmppConnectionService,
711 requestCode,
712 fullScreenIntent,
713 s()
714 ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
715 : PendingIntent.FLAG_UPDATE_CURRENT);
716 }
717
718 public void cancelIncomingCallNotification() {
719 cancel(INCOMING_CALL_NOTIFICATION_ID);
720 }
721
722 public boolean stopSoundAndVibration() {
723 final var jingleRtpConnection =
724 mXmppConnectionService.getJingleConnectionManager().getOngoingRtpConnection();
725 if (jingleRtpConnection == null) {
726 return false;
727 }
728 final var notificationManager =
729 mXmppConnectionService.getSystemService(NotificationManager.class);
730 if (Iterables.any(
731 Arrays.asList(notificationManager.getActiveNotifications()),
732 n -> n.getId() == INCOMING_CALL_NOTIFICATION_ID)) {
733 Log.d(Config.LOGTAG, "stopping sound and vibration for incoming call notification");
734 showIncomingCallNotification(
735 jingleRtpConnection.getId(), jingleRtpConnection.getMedia(), true);
736 return true;
737 }
738 return false;
739 }
740
741 public static void cancelIncomingCallNotification(final Context context) {
742 final NotificationManagerCompat notificationManager =
743 NotificationManagerCompat.from(context);
744 try {
745 notificationManager.cancel(INCOMING_CALL_NOTIFICATION_ID);
746 } catch (RuntimeException e) {
747 Log.d(Config.LOGTAG, "unable to cancel incoming call notification after crash", e);
748 }
749 }
750
751 private void pushNow(final Message message) {
752 mXmppConnectionService.updateUnreadCountBadge();
753 if (!notifyMessage(message)) {
754 Log.d(
755 Config.LOGTAG,
756 message.getConversation().getAccount().getJid().asBareJid()
757 + ": suppressing notification because turned off");
758 return;
759 }
760 final boolean isScreenLocked = mXmppConnectionService.isScreenLocked();
761 if (this.mIsInForeground
762 && !isScreenLocked
763 && this.mOpenConversation == message.getConversation()) {
764 Log.d(
765 Config.LOGTAG,
766 message.getConversation().getAccount().getJid().asBareJid()
767 + ": suppressing notification because conversation is open");
768 return;
769 }
770 synchronized (notifications) {
771 pushToStack(message);
772 final Conversational conversation = message.getConversation();
773 final Account account = conversation.getAccount();
774 final boolean doNotify =
775 (!(this.mIsInForeground && this.mOpenConversation == null) || isScreenLocked)
776 && !account.inGracePeriod()
777 && !this.inMiniGracePeriod(account);
778 updateNotification(doNotify, Collections.singletonList(conversation.getUuid()));
779 }
780 }
781
782 private void pushMissedCall(final Message message) {
783 final Conversational conversation = message.getConversation();
784 final MissedCallsInfo info = mMissedCalls.get(conversation);
785 if (info == null) {
786 mMissedCalls.put(conversation, new MissedCallsInfo(message.getTimeSent()));
787 } else {
788 info.newMissedCall(message.getTimeSent());
789 }
790 }
791
792 public void pushMissedCallNow(final Message message) {
793 synchronized (mMissedCalls) {
794 pushMissedCall(message);
795 updateMissedCallNotifications(Collections.singleton(message.getConversation()));
796 }
797 }
798
799 public void clear(final Conversation conversation) {
800 clearMessages(conversation);
801 clearMissedCalls(conversation);
802 }
803
804 public void clearMessages() {
805 synchronized (notifications) {
806 for (ArrayList<Message> messages : notifications.values()) {
807 markAsReadIfHasDirectReply(messages);
808 }
809 notifications.clear();
810 updateNotification(false);
811 }
812 }
813
814 public void clearMessages(final Conversation conversation) {
815 synchronized (this.mBacklogMessageCounter) {
816 this.mBacklogMessageCounter.remove(conversation);
817 }
818 synchronized (notifications) {
819 markAsReadIfHasDirectReply(conversation);
820 if (notifications.remove(conversation.getUuid()) != null) {
821 cancel(conversation.getUuid(), NOTIFICATION_ID);
822 updateNotification(false, null, true);
823 }
824 }
825 }
826
827 public void clearMissedCall(final Message message) {
828 synchronized (mMissedCalls) {
829 final Iterator<Map.Entry<Conversational, MissedCallsInfo>> iterator =
830 mMissedCalls.entrySet().iterator();
831 while (iterator.hasNext()) {
832 final Map.Entry<Conversational, MissedCallsInfo> entry = iterator.next();
833 final Conversational conversational = entry.getKey();
834 final MissedCallsInfo missedCallsInfo = entry.getValue();
835 if (conversational.getUuid().equals(message.getConversation().getUuid())) {
836 if (missedCallsInfo.removeMissedCall()) {
837 cancel(conversational.getUuid(), MISSED_CALL_NOTIFICATION_ID);
838 Log.d(
839 Config.LOGTAG,
840 conversational.getAccount().getJid().asBareJid()
841 + ": dismissed missed call because call was picked up on"
842 + " other device");
843 iterator.remove();
844 }
845 }
846 }
847 updateMissedCallNotifications(null);
848 }
849 }
850
851 public void clearMissedCalls() {
852 synchronized (mMissedCalls) {
853 for (final Conversational conversation : mMissedCalls.keySet()) {
854 cancel(conversation.getUuid(), MISSED_CALL_NOTIFICATION_ID);
855 }
856 mMissedCalls.clear();
857 updateMissedCallNotifications(null);
858 }
859 }
860
861 public void clearMissedCalls(final Conversation conversation) {
862 synchronized (mMissedCalls) {
863 if (mMissedCalls.remove(conversation) != null) {
864 cancel(conversation.getUuid(), MISSED_CALL_NOTIFICATION_ID);
865 updateMissedCallNotifications(null);
866 }
867 }
868 }
869
870 private void markAsReadIfHasDirectReply(final Conversation conversation) {
871 markAsReadIfHasDirectReply(notifications.get(conversation.getUuid()));
872 }
873
874 private void markAsReadIfHasDirectReply(final ArrayList<Message> messages) {
875 if (messages != null && !messages.isEmpty()) {
876 Message last = messages.get(messages.size() - 1);
877 if (last.getStatus() != Message.STATUS_RECEIVED) {
878 if (mXmppConnectionService.markRead((Conversation) last.getConversation(), false)) {
879 mXmppConnectionService.updateConversationUi();
880 }
881 }
882 }
883 }
884
885 private void setNotificationColor(final Builder mBuilder) {
886 mBuilder.setColor(ContextCompat.getColor(mXmppConnectionService, R.color.green_600));
887 }
888
889 public void updateNotification() {
890 synchronized (notifications) {
891 updateNotification(false);
892 }
893 }
894
895 private void updateNotification(final boolean notify) {
896 updateNotification(notify, null, false);
897 }
898
899 private void updateNotification(final boolean notify, final List<String> conversations) {
900 updateNotification(notify, conversations, false);
901 }
902
903 private void updateNotification(
904 final boolean notify, final List<String> conversations, final boolean summaryOnly) {
905 final SharedPreferences preferences =
906 PreferenceManager.getDefaultSharedPreferences(mXmppConnectionService);
907
908 final boolean notifyOnlyOneChild =
909 notify
910 && conversations != null
911 && conversations.size()
912 == 1; // if this check is changed to > 0 catchup messages will
913 // create one notification per conversation
914
915 if (notifications.isEmpty()) {
916 cancel(NOTIFICATION_ID);
917 } else {
918 if (notify) {
919 this.markLastNotification();
920 }
921 final Builder mBuilder;
922 if (notifications.size() == 1 && Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
923 mBuilder =
924 buildSingleConversations(notifications.values().iterator().next(), notify);
925 modifyForSoundVibrationAndLight(mBuilder, notify, preferences);
926 notify(NOTIFICATION_ID, mBuilder.build());
927 } else {
928 mBuilder = buildMultipleConversation(notify);
929 if (notifyOnlyOneChild) {
930 mBuilder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN);
931 }
932 modifyForSoundVibrationAndLight(mBuilder, notify, preferences);
933 if (!summaryOnly) {
934 for (Map.Entry<String, ArrayList<Message>> entry : notifications.entrySet()) {
935 String uuid = entry.getKey();
936 final boolean notifyThis =
937 notifyOnlyOneChild ? conversations.contains(uuid) : notify;
938 Builder singleBuilder =
939 buildSingleConversations(entry.getValue(), notifyThis);
940 if (!notifyOnlyOneChild) {
941 singleBuilder.setGroupAlertBehavior(
942 NotificationCompat.GROUP_ALERT_SUMMARY);
943 }
944 modifyForSoundVibrationAndLight(singleBuilder, notifyThis, preferences);
945 singleBuilder.setGroup(MESSAGES_GROUP);
946 setNotificationColor(singleBuilder);
947 notify(entry.getKey(), NOTIFICATION_ID, singleBuilder.build());
948 }
949 }
950 notify(NOTIFICATION_ID, mBuilder.build());
951 }
952 }
953 }
954
955 private void updateMissedCallNotifications(final Set<Conversational> update) {
956 if (mMissedCalls.isEmpty()) {
957 cancel(MISSED_CALL_NOTIFICATION_ID);
958 return;
959 }
960 if (mMissedCalls.size() == 1 && Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
961 final Conversational conversation = mMissedCalls.keySet().iterator().next();
962 final MissedCallsInfo info = mMissedCalls.values().iterator().next();
963 final Notification notification = missedCall(conversation, info);
964 notify(MISSED_CALL_NOTIFICATION_ID, notification);
965 } else {
966 final Notification summary = missedCallsSummary();
967 notify(MISSED_CALL_NOTIFICATION_ID, summary);
968 if (update != null) {
969 for (final Conversational conversation : update) {
970 final MissedCallsInfo info = mMissedCalls.get(conversation);
971 if (info != null) {
972 final Notification notification = missedCall(conversation, info);
973 notify(conversation.getUuid(), MISSED_CALL_NOTIFICATION_ID, notification);
974 }
975 }
976 }
977 }
978 }
979
980 private void modifyForSoundVibrationAndLight(
981 final Builder mBuilder, final boolean notify, final SharedPreferences preferences) {
982 final Resources resources = mXmppConnectionService.getResources();
983 final String ringtone =
984 preferences.getString(
985 AppSettings.NOTIFICATION_RINGTONE,
986 resources.getString(R.string.notification_ringtone));
987 final boolean vibrate =
988 preferences.getBoolean(
989 AppSettings.NOTIFICATION_VIBRATE,
990 resources.getBoolean(R.bool.vibrate_on_notification));
991 final boolean led =
992 preferences.getBoolean(
993 AppSettings.NOTIFICATION_LED, resources.getBoolean(R.bool.led));
994 final boolean headsup =
995 preferences.getBoolean(
996 AppSettings.NOTIFICATION_HEADS_UP,
997 resources.getBoolean(R.bool.headsup_notifications));
998 if (notify) {
999 if (vibrate) {
1000 final int dat = 70;
1001 final long[] pattern = {0, 3 * dat, dat, dat};
1002 mBuilder.setVibrate(pattern);
1003 } else {
1004 mBuilder.setVibrate(new long[] {0});
1005 }
1006 Uri uri = Uri.parse(ringtone);
1007 try {
1008 mBuilder.setSound(fixRingtoneUri(uri));
1009 } catch (SecurityException e) {
1010 Log.d(Config.LOGTAG, "unable to use custom notification sound " + uri.toString());
1011 }
1012 } else {
1013 mBuilder.setLocalOnly(true);
1014 }
1015 mBuilder.setCategory(Notification.CATEGORY_MESSAGE);
1016 mBuilder.setPriority(
1017 notify
1018 ? (headsup
1019 ? NotificationCompat.PRIORITY_HIGH
1020 : NotificationCompat.PRIORITY_DEFAULT)
1021 : NotificationCompat.PRIORITY_LOW);
1022 setNotificationColor(mBuilder);
1023 mBuilder.setDefaults(0);
1024 if (led) {
1025 mBuilder.setLights(LED_COLOR, 2000, 3000);
1026 }
1027 }
1028
1029 private void modifyIncomingCall(final Builder mBuilder) {
1030 mBuilder.setPriority(NotificationCompat.PRIORITY_HIGH);
1031 setNotificationColor(mBuilder);
1032 mBuilder.setLights(LED_COLOR, 2000, 3000);
1033 }
1034
1035 private Uri fixRingtoneUri(Uri uri) {
1036 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && "file".equals(uri.getScheme())) {
1037 return FileBackend.getUriForFile(mXmppConnectionService, new File(uri.getPath()));
1038 } else {
1039 return uri;
1040 }
1041 }
1042
1043 private Notification missedCallsSummary() {
1044 final Builder publicBuilder = buildMissedCallsSummary(true);
1045 final Builder builder = buildMissedCallsSummary(false);
1046 builder.setPublicVersion(publicBuilder.build());
1047 return builder.build();
1048 }
1049
1050 private Builder buildMissedCallsSummary(boolean publicVersion) {
1051 final Builder builder =
1052 new NotificationCompat.Builder(mXmppConnectionService, "missed_calls");
1053 int totalCalls = 0;
1054 final List<String> names = new ArrayList<>();
1055 long lastTime = 0;
1056 for (final Map.Entry<Conversational, MissedCallsInfo> entry : mMissedCalls.entrySet()) {
1057 final Conversational conversation = entry.getKey();
1058 final MissedCallsInfo missedCallsInfo = entry.getValue();
1059 names.add(conversation.getContact().getDisplayName());
1060 totalCalls += missedCallsInfo.getNumberOfCalls();
1061 lastTime = Math.max(lastTime, missedCallsInfo.getLastTime());
1062 }
1063 final String title =
1064 (totalCalls == 1)
1065 ? mXmppConnectionService.getString(R.string.missed_call)
1066 : (mMissedCalls.size() == 1)
1067 ? mXmppConnectionService
1068 .getResources()
1069 .getQuantityString(
1070 R.plurals.n_missed_calls, totalCalls, totalCalls)
1071 : mXmppConnectionService
1072 .getResources()
1073 .getQuantityString(
1074 R.plurals.n_missed_calls_from_m_contacts,
1075 mMissedCalls.size(),
1076 totalCalls,
1077 mMissedCalls.size());
1078 builder.setContentTitle(title);
1079 builder.setTicker(title);
1080 if (!publicVersion) {
1081 builder.setContentText(Joiner.on(", ").join(names));
1082 }
1083 builder.setSmallIcon(R.drawable.ic_call_missed_24db);
1084 builder.setGroupSummary(true);
1085 builder.setGroup(MISSED_CALLS_GROUP);
1086 builder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN);
1087 builder.setCategory(NotificationCompat.CATEGORY_CALL);
1088 builder.setWhen(lastTime);
1089 if (!mMissedCalls.isEmpty()) {
1090 final Conversational firstConversation = mMissedCalls.keySet().iterator().next();
1091 builder.setContentIntent(createContentIntent(firstConversation));
1092 }
1093 builder.setDeleteIntent(createMissedCallsDeleteIntent(null));
1094 modifyMissedCall(builder);
1095 return builder;
1096 }
1097
1098 private Notification missedCall(final Conversational conversation, final MissedCallsInfo info) {
1099 final Builder publicBuilder = buildMissedCall(conversation, info, true);
1100 final Builder builder = buildMissedCall(conversation, info, false);
1101 builder.setPublicVersion(publicBuilder.build());
1102 return builder.build();
1103 }
1104
1105 private Builder buildMissedCall(
1106 final Conversational conversation, final MissedCallsInfo info, boolean publicVersion) {
1107 final Builder builder =
1108 new NotificationCompat.Builder(mXmppConnectionService, "missed_calls");
1109 final String title =
1110 (info.getNumberOfCalls() == 1)
1111 ? mXmppConnectionService.getString(R.string.missed_call)
1112 : mXmppConnectionService
1113 .getResources()
1114 .getQuantityString(
1115 R.plurals.n_missed_calls,
1116 info.getNumberOfCalls(),
1117 info.getNumberOfCalls());
1118 builder.setContentTitle(title);
1119 final String name = conversation.getContact().getDisplayName();
1120 if (publicVersion) {
1121 builder.setTicker(title);
1122 } else {
1123 builder.setTicker(
1124 mXmppConnectionService
1125 .getResources()
1126 .getQuantityString(
1127 R.plurals.n_missed_calls_from_x,
1128 info.getNumberOfCalls(),
1129 info.getNumberOfCalls(),
1130 name));
1131 builder.setContentText(name);
1132 }
1133 builder.setSmallIcon(R.drawable.ic_call_missed_24db);
1134 builder.setGroup(MISSED_CALLS_GROUP);
1135 builder.setCategory(NotificationCompat.CATEGORY_CALL);
1136 builder.setWhen(info.getLastTime());
1137 builder.setContentIntent(createContentIntent(conversation));
1138 builder.setDeleteIntent(createMissedCallsDeleteIntent(conversation));
1139 if (!publicVersion && conversation instanceof Conversation) {
1140 builder.setLargeIcon(
1141 mXmppConnectionService
1142 .getAvatarService()
1143 .get(
1144 (Conversation) conversation,
1145 AvatarService.getSystemUiAvatarSize(mXmppConnectionService)));
1146 }
1147 modifyMissedCall(builder);
1148 return builder;
1149 }
1150
1151 private void modifyMissedCall(final Builder builder) {
1152 final SharedPreferences preferences =
1153 PreferenceManager.getDefaultSharedPreferences(mXmppConnectionService);
1154 final Resources resources = mXmppConnectionService.getResources();
1155 final boolean led = preferences.getBoolean("led", resources.getBoolean(R.bool.led));
1156 if (led) {
1157 builder.setLights(LED_COLOR, 2000, 3000);
1158 }
1159 builder.setPriority(NotificationCompat.PRIORITY_HIGH);
1160 builder.setSound(null);
1161 setNotificationColor(builder);
1162 }
1163
1164 private Builder buildMultipleConversation(final boolean notify) {
1165 final Builder mBuilder =
1166 new NotificationCompat.Builder(
1167 mXmppConnectionService,
1168 notify ? MESSAGES_NOTIFICATION_CHANNEL : "silent_messages");
1169 final NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle();
1170 style.setBigContentTitle(
1171 mXmppConnectionService
1172 .getResources()
1173 .getQuantityString(
1174 R.plurals.x_unread_conversations,
1175 notifications.size(),
1176 notifications.size()));
1177 final List<String> names = new ArrayList<>();
1178 Conversation conversation = null;
1179 for (final ArrayList<Message> messages : notifications.values()) {
1180 if (messages.isEmpty()) {
1181 continue;
1182 }
1183 conversation = (Conversation) messages.get(0).getConversation();
1184 final String name = conversation.getName().toString();
1185 SpannableString styledString;
1186 if (Config.HIDE_MESSAGE_TEXT_IN_NOTIFICATION) {
1187 int count = messages.size();
1188 styledString =
1189 new SpannableString(
1190 name
1191 + ": "
1192 + mXmppConnectionService
1193 .getResources()
1194 .getQuantityString(
1195 R.plurals.x_messages, count, count));
1196 styledString.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), 0);
1197 style.addLine(styledString);
1198 } else {
1199 styledString =
1200 new SpannableString(
1201 name
1202 + ": "
1203 + UIHelper.getMessagePreview(
1204 mXmppConnectionService, messages.get(0))
1205 .first);
1206 styledString.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), 0);
1207 style.addLine(styledString);
1208 }
1209 names.add(name);
1210 }
1211 final String contentTitle =
1212 mXmppConnectionService
1213 .getResources()
1214 .getQuantityString(
1215 R.plurals.x_unread_conversations,
1216 notifications.size(),
1217 notifications.size());
1218 mBuilder.setContentTitle(contentTitle);
1219 mBuilder.setTicker(contentTitle);
1220 mBuilder.setContentText(Joiner.on(", ").join(names));
1221 mBuilder.setStyle(style);
1222 if (conversation != null) {
1223 mBuilder.setContentIntent(createContentIntent(conversation));
1224 }
1225 mBuilder.setGroupSummary(true);
1226 mBuilder.setGroup(MESSAGES_GROUP);
1227 mBuilder.setDeleteIntent(createDeleteIntent(null));
1228 mBuilder.setSmallIcon(R.drawable.ic_app_icon_notification);
1229 return mBuilder;
1230 }
1231
1232 private Builder buildSingleConversations(
1233 final ArrayList<Message> messages, final boolean notify) {
1234 final var channel = notify ? MESSAGES_NOTIFICATION_CHANNEL : "silent_messages";
1235 final Builder notificationBuilder =
1236 new NotificationCompat.Builder(mXmppConnectionService, channel);
1237 if (messages.isEmpty()) {
1238 return notificationBuilder;
1239 }
1240 final Conversation conversation = (Conversation) messages.get(0).getConversation();
1241 notificationBuilder.setLargeIcon(
1242 mXmppConnectionService
1243 .getAvatarService()
1244 .get(
1245 conversation,
1246 AvatarService.getSystemUiAvatarSize(mXmppConnectionService)));
1247 notificationBuilder.setContentTitle(conversation.getName());
1248 if (Config.HIDE_MESSAGE_TEXT_IN_NOTIFICATION) {
1249 int count = messages.size();
1250 notificationBuilder.setContentText(
1251 mXmppConnectionService
1252 .getResources()
1253 .getQuantityString(R.plurals.x_messages, count, count));
1254 } else {
1255 final Message message;
1256 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P
1257 && (message = getImage(messages)) != null) {
1258 modifyForImage(notificationBuilder, message, messages);
1259 } else {
1260 modifyForTextOnly(notificationBuilder, messages);
1261 }
1262 RemoteInput remoteInput =
1263 new RemoteInput.Builder("text_reply")
1264 .setLabel(UIHelper.getMessageHint(mXmppConnectionService, conversation))
1265 .build();
1266 PendingIntent markAsReadPendingIntent = createReadPendingIntent(conversation);
1267 NotificationCompat.Action markReadAction =
1268 new NotificationCompat.Action.Builder(
1269 R.drawable.ic_mark_chat_read_24dp,
1270 mXmppConnectionService.getString(R.string.mark_as_read),
1271 markAsReadPendingIntent)
1272 .setSemanticAction(
1273 NotificationCompat.Action.SEMANTIC_ACTION_MARK_AS_READ)
1274 .setShowsUserInterface(false)
1275 .build();
1276 final String replyLabel = mXmppConnectionService.getString(R.string.reply);
1277 final String lastMessageUuid = Iterables.getLast(messages).getUuid();
1278 final NotificationCompat.Action replyAction =
1279 new NotificationCompat.Action.Builder(
1280 R.drawable.ic_send_24dp,
1281 replyLabel,
1282 createReplyIntent(conversation, lastMessageUuid, false))
1283 .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY)
1284 .setShowsUserInterface(false)
1285 .addRemoteInput(remoteInput)
1286 .build();
1287 final NotificationCompat.Action wearReplyAction =
1288 new NotificationCompat.Action.Builder(
1289 R.drawable.ic_reply_24dp,
1290 replyLabel,
1291 createReplyIntent(conversation, lastMessageUuid, true))
1292 .addRemoteInput(remoteInput)
1293 .build();
1294 notificationBuilder.extend(
1295 new NotificationCompat.WearableExtender().addAction(wearReplyAction));
1296 int addedActionsCount = 1;
1297 notificationBuilder.addAction(markReadAction);
1298 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
1299 notificationBuilder.addAction(replyAction);
1300 ++addedActionsCount;
1301 }
1302
1303 if (displaySnoozeAction(messages)) {
1304 String label = mXmppConnectionService.getString(R.string.snooze);
1305 PendingIntent pendingSnoozeIntent = createSnoozeIntent(conversation);
1306 NotificationCompat.Action snoozeAction =
1307 new NotificationCompat.Action.Builder(
1308 R.drawable.ic_notifications_paused_24dp,
1309 label,
1310 pendingSnoozeIntent)
1311 .build();
1312 notificationBuilder.addAction(snoozeAction);
1313 ++addedActionsCount;
1314 }
1315 if (addedActionsCount < 3) {
1316 final Message firstLocationMessage = getFirstLocationMessage(messages);
1317 if (firstLocationMessage != null) {
1318 final PendingIntent pendingShowLocationIntent =
1319 createShowLocationIntent(firstLocationMessage);
1320 if (pendingShowLocationIntent != null) {
1321 final String label =
1322 mXmppConnectionService
1323 .getResources()
1324 .getString(R.string.show_location);
1325 NotificationCompat.Action locationAction =
1326 new NotificationCompat.Action.Builder(
1327 R.drawable.ic_location_pin_24dp,
1328 label,
1329 pendingShowLocationIntent)
1330 .build();
1331 notificationBuilder.addAction(locationAction);
1332 ++addedActionsCount;
1333 }
1334 }
1335 }
1336 if (addedActionsCount < 3) {
1337 Message firstDownloadableMessage = getFirstDownloadableMessage(messages);
1338 if (firstDownloadableMessage != null) {
1339 String label =
1340 mXmppConnectionService
1341 .getResources()
1342 .getString(
1343 R.string.download_x_file,
1344 UIHelper.getFileDescriptionString(
1345 mXmppConnectionService,
1346 firstDownloadableMessage));
1347 PendingIntent pendingDownloadIntent =
1348 createDownloadIntent(firstDownloadableMessage);
1349 NotificationCompat.Action downloadAction =
1350 new NotificationCompat.Action.Builder(
1351 R.drawable.ic_download_24dp,
1352 label,
1353 pendingDownloadIntent)
1354 .build();
1355 notificationBuilder.addAction(downloadAction);
1356 ++addedActionsCount;
1357 }
1358 }
1359 }
1360 final ShortcutInfoCompat info;
1361 if (conversation.getMode() == Conversation.MODE_SINGLE) {
1362 final Contact contact = conversation.getContact();
1363 final Uri systemAccount = contact.getSystemAccount();
1364 if (systemAccount != null) {
1365 notificationBuilder.addPerson(systemAccount.toString());
1366 }
1367 info =
1368 mXmppConnectionService
1369 .getShortcutService()
1370 .getShortcutInfo(contact, conversation.getUuid());
1371 } else {
1372 info =
1373 mXmppConnectionService
1374 .getShortcutService()
1375 .getShortcutInfo(conversation.getMucOptions());
1376 }
1377 notificationBuilder.setWhen(conversation.getLatestMessage().getTimeSent());
1378 notificationBuilder.setSmallIcon(R.drawable.ic_app_icon_notification);
1379 notificationBuilder.setDeleteIntent(createDeleteIntent(conversation));
1380 notificationBuilder.setContentIntent(createContentIntent(conversation));
1381 if (channel.equals(MESSAGES_NOTIFICATION_CHANNEL)) {
1382 // when do not want 'customized' notifications for silent notifications in their
1383 // respective channels
1384 notificationBuilder.setShortcutInfo(info);
1385 if (Build.VERSION.SDK_INT >= 30) {
1386 mXmppConnectionService
1387 .getSystemService(ShortcutManager.class)
1388 .pushDynamicShortcut(info.toShortcutInfo());
1389 }
1390 }
1391 return notificationBuilder;
1392 }
1393
1394 private void modifyForImage(
1395 final Builder builder, final Message message, final ArrayList<Message> messages) {
1396 try {
1397 final Bitmap bitmap =
1398 mXmppConnectionService
1399 .getFileBackend()
1400 .getThumbnail(message, getPixel(288), false);
1401 final ArrayList<Message> tmp = new ArrayList<>();
1402 for (final Message msg : messages) {
1403 if (msg.getType() == Message.TYPE_TEXT && msg.getTransferable() == null) {
1404 tmp.add(msg);
1405 }
1406 }
1407 final BigPictureStyle bigPictureStyle = new NotificationCompat.BigPictureStyle();
1408 bigPictureStyle.bigPicture(bitmap);
1409 if (tmp.isEmpty()) {
1410 final String description =
1411 UIHelper.getFileDescriptionString(mXmppConnectionService, message);
1412 builder.setContentText(description);
1413 builder.setTicker(description);
1414 } else {
1415 final CharSequence text = getMergedBodies(tmp);
1416 bigPictureStyle.setSummaryText(text);
1417 builder.setContentText(text);
1418 builder.setTicker(text);
1419 }
1420 builder.setStyle(bigPictureStyle);
1421 } catch (final IOException e) {
1422 modifyForTextOnly(builder, messages);
1423 }
1424 }
1425
1426 private Person getPerson(Message message) {
1427 final Contact contact = message.getContact();
1428 final Person.Builder builder = new Person.Builder();
1429 if (contact != null) {
1430 builder.setName(contact.getDisplayName());
1431 final Uri uri = contact.getSystemAccount();
1432 if (uri != null) {
1433 builder.setUri(uri.toString());
1434 }
1435 } else {
1436 builder.setName(UIHelper.getMessageDisplayName(message));
1437 }
1438 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
1439 builder.setIcon(
1440 IconCompat.createWithBitmap(
1441 mXmppConnectionService
1442 .getAvatarService()
1443 .get(
1444 message,
1445 AvatarService.getSystemUiAvatarSize(
1446 mXmppConnectionService),
1447 false)));
1448 }
1449 return builder.build();
1450 }
1451
1452 private void modifyForTextOnly(final Builder builder, final ArrayList<Message> messages) {
1453 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
1454 final Conversation conversation = (Conversation) messages.get(0).getConversation();
1455 final Person.Builder meBuilder =
1456 new Person.Builder().setName(mXmppConnectionService.getString(R.string.me));
1457 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
1458 meBuilder.setIcon(
1459 IconCompat.createWithBitmap(
1460 mXmppConnectionService
1461 .getAvatarService()
1462 .get(
1463 conversation.getAccount(),
1464 AvatarService.getSystemUiAvatarSize(
1465 mXmppConnectionService))));
1466 }
1467 final Person me = meBuilder.build();
1468 NotificationCompat.MessagingStyle messagingStyle =
1469 new NotificationCompat.MessagingStyle(me);
1470 final boolean multiple = conversation.getMode() == Conversation.MODE_MULTI;
1471 if (multiple) {
1472 messagingStyle.setConversationTitle(conversation.getName());
1473 }
1474 for (Message message : messages) {
1475 final Person sender =
1476 message.getStatus() == Message.STATUS_RECEIVED ? getPerson(message) : null;
1477 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && isImageMessage(message)) {
1478 final Uri dataUri =
1479 FileBackend.getMediaUri(
1480 mXmppConnectionService,
1481 mXmppConnectionService.getFileBackend().getFile(message));
1482 NotificationCompat.MessagingStyle.Message imageMessage =
1483 new NotificationCompat.MessagingStyle.Message(
1484 UIHelper.getMessagePreview(mXmppConnectionService, message)
1485 .first,
1486 message.getTimeSent(),
1487 sender);
1488 if (dataUri != null) {
1489 imageMessage.setData(message.getMimeType(), dataUri);
1490 }
1491 messagingStyle.addMessage(imageMessage);
1492 } else {
1493 messagingStyle.addMessage(
1494 UIHelper.getMessagePreview(mXmppConnectionService, message).first,
1495 message.getTimeSent(),
1496 sender);
1497 }
1498 }
1499 messagingStyle.setGroupConversation(multiple);
1500 builder.setStyle(messagingStyle);
1501 } else {
1502 if (messages.get(0).getConversation().getMode() == Conversation.MODE_SINGLE) {
1503 builder.setStyle(
1504 new NotificationCompat.BigTextStyle().bigText(getMergedBodies(messages)));
1505 final CharSequence preview =
1506 UIHelper.getMessagePreview(
1507 mXmppConnectionService, messages.get(messages.size() - 1))
1508 .first;
1509 builder.setContentText(preview);
1510 builder.setTicker(preview);
1511 builder.setNumber(messages.size());
1512 } else {
1513 final NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle();
1514 SpannableString styledString;
1515 for (Message message : messages) {
1516 final String name = UIHelper.getMessageDisplayName(message);
1517 styledString = new SpannableString(name + ": " + message.getBody());
1518 styledString.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), 0);
1519 style.addLine(styledString);
1520 }
1521 builder.setStyle(style);
1522 int count = messages.size();
1523 if (count == 1) {
1524 final String name = UIHelper.getMessageDisplayName(messages.get(0));
1525 styledString = new SpannableString(name + ": " + messages.get(0).getBody());
1526 styledString.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), 0);
1527 builder.setContentText(styledString);
1528 builder.setTicker(styledString);
1529 } else {
1530 final String text =
1531 mXmppConnectionService
1532 .getResources()
1533 .getQuantityString(R.plurals.x_messages, count, count);
1534 builder.setContentText(text);
1535 builder.setTicker(text);
1536 }
1537 }
1538 }
1539 }
1540
1541 private Message getImage(final Iterable<Message> messages) {
1542 Message image = null;
1543 for (final Message message : messages) {
1544 if (message.getStatus() != Message.STATUS_RECEIVED) {
1545 return null;
1546 }
1547 if (isImageMessage(message)) {
1548 image = message;
1549 }
1550 }
1551 return image;
1552 }
1553
1554 private Message getFirstDownloadableMessage(final Iterable<Message> messages) {
1555 for (final Message message : messages) {
1556 if (message.getTransferable() != null
1557 || (message.getType() == Message.TYPE_TEXT && message.treatAsDownloadable())) {
1558 return message;
1559 }
1560 }
1561 return null;
1562 }
1563
1564 private Message getFirstLocationMessage(final Iterable<Message> messages) {
1565 for (final Message message : messages) {
1566 if (message.isGeoUri()) {
1567 return message;
1568 }
1569 }
1570 return null;
1571 }
1572
1573 private CharSequence getMergedBodies(final ArrayList<Message> messages) {
1574 final StringBuilder text = new StringBuilder();
1575 for (Message message : messages) {
1576 if (text.length() != 0) {
1577 text.append("\n");
1578 }
1579 text.append(UIHelper.getMessagePreview(mXmppConnectionService, message).first);
1580 }
1581 return text.toString();
1582 }
1583
1584 private PendingIntent createShowLocationIntent(final Message message) {
1585 Iterable<Intent> intents =
1586 GeoHelper.createGeoIntentsFromMessage(mXmppConnectionService, message);
1587 for (final Intent intent : intents) {
1588 if (intent.resolveActivity(mXmppConnectionService.getPackageManager()) != null) {
1589 return PendingIntent.getActivity(
1590 mXmppConnectionService,
1591 generateRequestCode(message.getConversation(), 18),
1592 intent,
1593 s()
1594 ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1595 : PendingIntent.FLAG_UPDATE_CURRENT);
1596 }
1597 }
1598 return null;
1599 }
1600
1601 private PendingIntent createContentIntent(
1602 final String conversationUuid, final String downloadMessageUuid) {
1603 final Intent viewConversationIntent =
1604 new Intent(mXmppConnectionService, ConversationsActivity.class);
1605 viewConversationIntent.setAction(ConversationsActivity.ACTION_VIEW_CONVERSATION);
1606 viewConversationIntent.putExtra(ConversationsActivity.EXTRA_CONVERSATION, conversationUuid);
1607 if (downloadMessageUuid != null) {
1608 viewConversationIntent.putExtra(
1609 ConversationsActivity.EXTRA_DOWNLOAD_UUID, downloadMessageUuid);
1610 return PendingIntent.getActivity(
1611 mXmppConnectionService,
1612 generateRequestCode(conversationUuid, 8),
1613 viewConversationIntent,
1614 s()
1615 ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1616 : PendingIntent.FLAG_UPDATE_CURRENT);
1617 } else {
1618 return PendingIntent.getActivity(
1619 mXmppConnectionService,
1620 generateRequestCode(conversationUuid, 10),
1621 viewConversationIntent,
1622 s()
1623 ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1624 : PendingIntent.FLAG_UPDATE_CURRENT);
1625 }
1626 }
1627
1628 private int generateRequestCode(String uuid, int actionId) {
1629 return (actionId * NOTIFICATION_ID_MULTIPLIER)
1630 + (uuid.hashCode() % NOTIFICATION_ID_MULTIPLIER);
1631 }
1632
1633 private int generateRequestCode(Conversational conversation, int actionId) {
1634 return generateRequestCode(conversation.getUuid(), actionId);
1635 }
1636
1637 private PendingIntent createDownloadIntent(final Message message) {
1638 return createContentIntent(message.getConversationUuid(), message.getUuid());
1639 }
1640
1641 private PendingIntent createContentIntent(final Conversational conversation) {
1642 return createContentIntent(conversation.getUuid(), null);
1643 }
1644
1645 private PendingIntent createDeleteIntent(final Conversation conversation) {
1646 final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
1647 intent.setAction(XmppConnectionService.ACTION_CLEAR_MESSAGE_NOTIFICATION);
1648 if (conversation != null) {
1649 intent.putExtra("uuid", conversation.getUuid());
1650 return PendingIntent.getService(
1651 mXmppConnectionService,
1652 generateRequestCode(conversation, 20),
1653 intent,
1654 s()
1655 ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1656 : PendingIntent.FLAG_UPDATE_CURRENT);
1657 }
1658 return PendingIntent.getService(
1659 mXmppConnectionService,
1660 0,
1661 intent,
1662 s()
1663 ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1664 : PendingIntent.FLAG_UPDATE_CURRENT);
1665 }
1666
1667 private PendingIntent createMissedCallsDeleteIntent(final Conversational conversation) {
1668 final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
1669 intent.setAction(XmppConnectionService.ACTION_CLEAR_MISSED_CALL_NOTIFICATION);
1670 if (conversation != null) {
1671 intent.putExtra("uuid", conversation.getUuid());
1672 return PendingIntent.getService(
1673 mXmppConnectionService,
1674 generateRequestCode(conversation, 21),
1675 intent,
1676 s()
1677 ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1678 : PendingIntent.FLAG_UPDATE_CURRENT);
1679 }
1680 return PendingIntent.getService(
1681 mXmppConnectionService,
1682 1,
1683 intent,
1684 s()
1685 ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1686 : PendingIntent.FLAG_UPDATE_CURRENT);
1687 }
1688
1689 private PendingIntent createReplyIntent(
1690 final Conversation conversation,
1691 final String lastMessageUuid,
1692 final boolean dismissAfterReply) {
1693 final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
1694 intent.setAction(XmppConnectionService.ACTION_REPLY_TO_CONVERSATION);
1695 intent.putExtra("uuid", conversation.getUuid());
1696 intent.putExtra("dismiss_notification", dismissAfterReply);
1697 intent.putExtra("last_message_uuid", lastMessageUuid);
1698 final int id = generateRequestCode(conversation, dismissAfterReply ? 12 : 14);
1699 return PendingIntent.getService(
1700 mXmppConnectionService,
1701 id,
1702 intent,
1703 s()
1704 ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1705 : PendingIntent.FLAG_UPDATE_CURRENT);
1706 }
1707
1708 private PendingIntent createReadPendingIntent(Conversation conversation) {
1709 final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
1710 intent.setAction(XmppConnectionService.ACTION_MARK_AS_READ);
1711 intent.putExtra("uuid", conversation.getUuid());
1712 intent.setPackage(mXmppConnectionService.getPackageName());
1713 return PendingIntent.getService(
1714 mXmppConnectionService,
1715 generateRequestCode(conversation, 16),
1716 intent,
1717 s()
1718 ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1719 : PendingIntent.FLAG_UPDATE_CURRENT);
1720 }
1721
1722 private PendingIntent createCallAction(String sessionId, final String action, int requestCode) {
1723 return pendingServiceIntent(
1724 mXmppConnectionService,
1725 action,
1726 requestCode,
1727 ImmutableMap.of(RtpSessionActivity.EXTRA_SESSION_ID, sessionId));
1728 }
1729
1730 private PendingIntent createSnoozeIntent(final Conversation conversation) {
1731 return pendingServiceIntent(
1732 mXmppConnectionService,
1733 XmppConnectionService.ACTION_SNOOZE,
1734 generateRequestCode(conversation, 22),
1735 ImmutableMap.of("uuid", conversation.getUuid()));
1736 }
1737
1738 private static PendingIntent pendingServiceIntent(
1739 final Context context, final String action, final int requestCode) {
1740 return pendingServiceIntent(context, action, requestCode, ImmutableMap.of());
1741 }
1742
1743 private static PendingIntent pendingServiceIntent(
1744 final Context context,
1745 final String action,
1746 final int requestCode,
1747 final Map<String, String> extras) {
1748 final Intent intent = new Intent(context, XmppConnectionService.class);
1749 intent.setAction(action);
1750 for (final Map.Entry<String, String> entry : extras.entrySet()) {
1751 intent.putExtra(entry.getKey(), entry.getValue());
1752 }
1753 return PendingIntent.getService(
1754 context,
1755 requestCode,
1756 intent,
1757 s()
1758 ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1759 : PendingIntent.FLAG_UPDATE_CURRENT);
1760 }
1761
1762 private boolean wasHighlightedOrPrivate(final Message message) {
1763 if (message.getConversation() instanceof Conversation conversation) {
1764 final String nick = conversation.getMucOptions().getActualNick();
1765 final Pattern highlight = generateNickHighlightPattern(nick);
1766 if (message.getBody() == null || nick == null) {
1767 return false;
1768 }
1769 final Matcher m = highlight.matcher(message.getBody());
1770 return (m.find() || message.isPrivateMessage());
1771 } else {
1772 return false;
1773 }
1774 }
1775
1776 public void setOpenConversation(final Conversation conversation) {
1777 this.mOpenConversation = conversation;
1778 }
1779
1780 public void setIsInForeground(final boolean foreground) {
1781 this.mIsInForeground = foreground;
1782 }
1783
1784 private int getPixel(final int dp) {
1785 final DisplayMetrics metrics = mXmppConnectionService.getResources().getDisplayMetrics();
1786 return ((int) (dp * metrics.density));
1787 }
1788
1789 private void markLastNotification() {
1790 this.mLastNotification = SystemClock.elapsedRealtime();
1791 }
1792
1793 private boolean inMiniGracePeriod(final Account account) {
1794 final int miniGrace =
1795 account.getStatus() == Account.State.ONLINE
1796 ? Config.MINI_GRACE_PERIOD
1797 : Config.MINI_GRACE_PERIOD * 2;
1798 return SystemClock.elapsedRealtime() < (this.mLastNotification + miniGrace);
1799 }
1800
1801 Notification createForegroundNotification() {
1802 final Notification.Builder mBuilder = new Notification.Builder(mXmppConnectionService);
1803 mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.app_name));
1804 final List<Account> accounts = mXmppConnectionService.getAccounts();
1805 final int enabled;
1806 final int connected;
1807 if (accounts == null) {
1808 enabled = 0;
1809 connected = 0;
1810 } else {
1811 enabled = Iterables.size(Iterables.filter(accounts, Account::isEnabled));
1812 connected = Iterables.size(Iterables.filter(accounts, Account::isOnlineAndConnected));
1813 }
1814 mBuilder.setContentText(
1815 mXmppConnectionService.getString(R.string.connected_accounts, connected, enabled));
1816 final PendingIntent openIntent = createOpenConversationsIntent();
1817 if (openIntent != null) {
1818 mBuilder.setContentIntent(openIntent);
1819 }
1820 mBuilder.setWhen(0)
1821 .setPriority(Notification.PRIORITY_MIN)
1822 .setSmallIcon(connected > 0 ? R.drawable.ic_link_24dp : R.drawable.ic_link_off_24dp)
1823 .setLocalOnly(true);
1824
1825 if (Compatibility.runsTwentySix()) {
1826 mBuilder.setChannelId("foreground");
1827 mBuilder.addAction(
1828 R.drawable.ic_logout_24dp,
1829 mXmppConnectionService.getString(R.string.log_out),
1830 pendingServiceIntent(
1831 mXmppConnectionService,
1832 XmppConnectionService.ACTION_TEMPORARILY_DISABLE,
1833 87));
1834 mBuilder.addAction(
1835 R.drawable.ic_notifications_off_24dp,
1836 mXmppConnectionService.getString(R.string.hide_notification),
1837 pendingNotificationSettingsIntent(mXmppConnectionService));
1838 }
1839
1840 return mBuilder.build();
1841 }
1842
1843 @RequiresApi(api = Build.VERSION_CODES.O)
1844 private static PendingIntent pendingNotificationSettingsIntent(final Context context) {
1845 final Intent intent = new Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS);
1846 intent.putExtra(Settings.EXTRA_APP_PACKAGE, context.getPackageName());
1847 intent.putExtra(Settings.EXTRA_CHANNEL_ID, "foreground");
1848 return PendingIntent.getActivity(
1849 context,
1850 89,
1851 intent,
1852 s()
1853 ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1854 : PendingIntent.FLAG_UPDATE_CURRENT);
1855 }
1856
1857 private PendingIntent createOpenConversationsIntent() {
1858 try {
1859 return PendingIntent.getActivity(
1860 mXmppConnectionService,
1861 0,
1862 new Intent(mXmppConnectionService, ConversationsActivity.class),
1863 s()
1864 ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1865 : PendingIntent.FLAG_UPDATE_CURRENT);
1866 } catch (RuntimeException e) {
1867 return null;
1868 }
1869 }
1870
1871 void updateErrorNotification() {
1872 if (Config.SUPPRESS_ERROR_NOTIFICATION) {
1873 cancel(ERROR_NOTIFICATION_ID);
1874 return;
1875 }
1876 final boolean showAllErrors = QuickConversationsService.isConversations();
1877 final List<Account> errors = new ArrayList<>();
1878 boolean torNotAvailable = false;
1879 for (final Account account : mXmppConnectionService.getAccounts()) {
1880 if (account.hasErrorStatus()
1881 && account.showErrorNotification()
1882 && (showAllErrors
1883 || account.getLastErrorStatus() == Account.State.UNAUTHORIZED)) {
1884 errors.add(account);
1885 torNotAvailable |= account.getStatus() == Account.State.TOR_NOT_AVAILABLE;
1886 }
1887 }
1888 if (mXmppConnectionService.foregroundNotificationNeedsUpdatingWhenErrorStateChanges()) {
1889 try {
1890 notify(FOREGROUND_NOTIFICATION_ID, createForegroundNotification());
1891 } catch (final RuntimeException e) {
1892 Log.d(
1893 Config.LOGTAG,
1894 "not refreshing foreground service notification because service has died",
1895 e);
1896 }
1897 }
1898 final Notification.Builder mBuilder = new Notification.Builder(mXmppConnectionService);
1899 if (errors.isEmpty()) {
1900 cancel(ERROR_NOTIFICATION_ID);
1901 return;
1902 } else if (errors.size() == 1) {
1903 mBuilder.setContentTitle(
1904 mXmppConnectionService.getString(R.string.problem_connecting_to_account));
1905 mBuilder.setContentText(errors.get(0).getJid().asBareJid().toEscapedString());
1906 } else {
1907 mBuilder.setContentTitle(
1908 mXmppConnectionService.getString(R.string.problem_connecting_to_accounts));
1909 mBuilder.setContentText(mXmppConnectionService.getString(R.string.touch_to_fix));
1910 }
1911 try {
1912 mBuilder.addAction(
1913 R.drawable.ic_autorenew_24dp,
1914 mXmppConnectionService.getString(R.string.try_again),
1915 pendingServiceIntent(
1916 mXmppConnectionService, XmppConnectionService.ACTION_TRY_AGAIN, 45));
1917 mBuilder.setDeleteIntent(
1918 pendingServiceIntent(
1919 mXmppConnectionService,
1920 XmppConnectionService.ACTION_DISMISS_ERROR_NOTIFICATIONS,
1921 69));
1922 } catch (final RuntimeException e) {
1923 Log.d(
1924 Config.LOGTAG,
1925 "not including some actions in error notification because service has died",
1926 e);
1927 }
1928 if (torNotAvailable) {
1929 if (TorServiceUtils.isOrbotInstalled(mXmppConnectionService)) {
1930 mBuilder.addAction(
1931 R.drawable.ic_play_circle_24dp,
1932 mXmppConnectionService.getString(R.string.start_orbot),
1933 PendingIntent.getActivity(
1934 mXmppConnectionService,
1935 147,
1936 TorServiceUtils.LAUNCH_INTENT,
1937 s()
1938 ? PendingIntent.FLAG_IMMUTABLE
1939 | PendingIntent.FLAG_UPDATE_CURRENT
1940 : PendingIntent.FLAG_UPDATE_CURRENT));
1941 } else {
1942 mBuilder.addAction(
1943 R.drawable.ic_download_24dp,
1944 mXmppConnectionService.getString(R.string.install_orbot),
1945 PendingIntent.getActivity(
1946 mXmppConnectionService,
1947 146,
1948 TorServiceUtils.INSTALL_INTENT,
1949 s()
1950 ? PendingIntent.FLAG_IMMUTABLE
1951 | PendingIntent.FLAG_UPDATE_CURRENT
1952 : PendingIntent.FLAG_UPDATE_CURRENT));
1953 }
1954 }
1955 mBuilder.setVisibility(Notification.VISIBILITY_PRIVATE);
1956 mBuilder.setSmallIcon(R.drawable.ic_warning_24dp);
1957 mBuilder.setLocalOnly(true);
1958 mBuilder.setPriority(Notification.PRIORITY_LOW);
1959 final Intent intent;
1960 if (AccountUtils.MANAGE_ACCOUNT_ACTIVITY != null) {
1961 intent = new Intent(mXmppConnectionService, AccountUtils.MANAGE_ACCOUNT_ACTIVITY);
1962 } else {
1963 intent = new Intent(mXmppConnectionService, EditAccountActivity.class);
1964 intent.putExtra("jid", errors.get(0).getJid().asBareJid().toEscapedString());
1965 intent.putExtra(EditAccountActivity.EXTRA_OPENED_FROM_NOTIFICATION, true);
1966 }
1967 mBuilder.setContentIntent(
1968 PendingIntent.getActivity(
1969 mXmppConnectionService,
1970 145,
1971 intent,
1972 s()
1973 ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1974 : PendingIntent.FLAG_UPDATE_CURRENT));
1975 if (Compatibility.runsTwentySix()) {
1976 mBuilder.setChannelId("error");
1977 }
1978 notify(ERROR_NOTIFICATION_ID, mBuilder.build());
1979 }
1980
1981 void updateFileAddingNotification(final int current, final Message message) {
1982
1983 final Notification notification = videoTranscoding(current, message);
1984 notify(ONGOING_VIDEO_TRANSCODING_NOTIFICATION_ID, notification);
1985 }
1986
1987 private Notification videoTranscoding(final int current, @Nullable final Message message) {
1988 final Notification.Builder builder = new Notification.Builder(mXmppConnectionService);
1989 builder.setContentTitle(mXmppConnectionService.getString(R.string.transcoding_video));
1990 if (current >= 0) {
1991 builder.setProgress(100, current, false);
1992 } else {
1993 builder.setProgress(100, 0, true);
1994 }
1995 builder.setSmallIcon(R.drawable.ic_hourglass_top_24dp);
1996 if (message != null) {
1997 builder.setContentIntent(createContentIntent(message.getConversation()));
1998 }
1999 builder.setOngoing(true);
2000 if (Compatibility.runsTwentySix()) {
2001 builder.setChannelId("compression");
2002 }
2003 return builder.build();
2004 }
2005
2006 public Notification getIndeterminateVideoTranscoding() {
2007 return videoTranscoding(-1, null);
2008 }
2009
2010 private void notify(final String tag, final int id, final Notification notification) {
2011 if (ActivityCompat.checkSelfPermission(
2012 mXmppConnectionService, Manifest.permission.POST_NOTIFICATIONS)
2013 != PackageManager.PERMISSION_GRANTED) {
2014 return;
2015 }
2016 final var notificationManager =
2017 mXmppConnectionService.getSystemService(NotificationManager.class);
2018 try {
2019 notificationManager.notify(tag, id, notification);
2020 } catch (final RuntimeException e) {
2021 Log.d(Config.LOGTAG, "unable to make notification", e);
2022 }
2023 }
2024
2025 public void notify(final int id, final Notification notification) {
2026 if (ActivityCompat.checkSelfPermission(
2027 mXmppConnectionService, Manifest.permission.POST_NOTIFICATIONS)
2028 != PackageManager.PERMISSION_GRANTED) {
2029 return;
2030 }
2031 final var notificationManager =
2032 mXmppConnectionService.getSystemService(NotificationManager.class);
2033 try {
2034 notificationManager.notify(id, notification);
2035 } catch (final RuntimeException e) {
2036 Log.d(Config.LOGTAG, "unable to make notification", e);
2037 }
2038 }
2039
2040 public void cancel(final int id) {
2041 final NotificationManagerCompat notificationManager =
2042 NotificationManagerCompat.from(mXmppConnectionService);
2043 try {
2044 notificationManager.cancel(id);
2045 } catch (RuntimeException e) {
2046 Log.d(Config.LOGTAG, "unable to cancel notification", e);
2047 }
2048 }
2049
2050 private void cancel(String tag, int id) {
2051 final NotificationManagerCompat notificationManager =
2052 NotificationManagerCompat.from(mXmppConnectionService);
2053 try {
2054 notificationManager.cancel(tag, id);
2055 } catch (RuntimeException e) {
2056 Log.d(Config.LOGTAG, "unable to cancel notification", e);
2057 }
2058 }
2059
2060 private static class MissedCallsInfo {
2061 private int numberOfCalls;
2062 private long lastTime;
2063
2064 MissedCallsInfo(final long time) {
2065 numberOfCalls = 1;
2066 lastTime = time;
2067 }
2068
2069 public void newMissedCall(final long time) {
2070 ++numberOfCalls;
2071 lastTime = time;
2072 }
2073
2074 public boolean removeMissedCall() {
2075 --numberOfCalls;
2076 return numberOfCalls <= 0;
2077 }
2078
2079 public int getNumberOfCalls() {
2080 return numberOfCalls;
2081 }
2082
2083 public long getLastTime() {
2084 return lastTime;
2085 }
2086 }
2087}