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