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