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