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