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