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