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