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