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;
31
32import androidx.annotation.Nullable;
33import androidx.annotation.RequiresApi;
34import androidx.core.app.ActivityCompat;
35import androidx.core.app.NotificationCompat;
36import androidx.core.app.NotificationCompat.BigPictureStyle;
37import androidx.core.app.NotificationCompat.Builder;
38import androidx.core.app.NotificationManagerCompat;
39import androidx.core.app.Person;
40import androidx.core.app.RemoteInput;
41import androidx.core.content.ContextCompat;
42import androidx.core.content.pm.ShortcutInfoCompat;
43import androidx.core.graphics.drawable.IconCompat;
44
45import com.google.common.base.Joiner;
46import com.google.common.base.Optional;
47import com.google.common.base.Splitter;
48import com.google.common.base.Strings;
49import com.google.common.collect.ImmutableMap;
50import com.google.common.collect.Iterables;
51import com.google.common.primitives.Ints;
52
53import eu.siacs.conversations.AppSettings;
54import eu.siacs.conversations.Config;
55import eu.siacs.conversations.R;
56import eu.siacs.conversations.entities.Account;
57import eu.siacs.conversations.entities.Contact;
58import eu.siacs.conversations.entities.Conversation;
59import eu.siacs.conversations.entities.Conversational;
60import eu.siacs.conversations.entities.Message;
61import eu.siacs.conversations.persistance.FileBackend;
62import eu.siacs.conversations.ui.ConversationsActivity;
63import eu.siacs.conversations.ui.EditAccountActivity;
64import eu.siacs.conversations.ui.RtpSessionActivity;
65import eu.siacs.conversations.utils.AccountUtils;
66import eu.siacs.conversations.utils.Compatibility;
67import eu.siacs.conversations.utils.GeoHelper;
68import eu.siacs.conversations.utils.TorServiceUtils;
69import eu.siacs.conversations.utils.UIHelper;
70import eu.siacs.conversations.xmpp.XmppConnection;
71import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection;
72import eu.siacs.conversations.xmpp.jingle.Media;
73
74import java.io.File;
75import java.io.IOException;
76import java.util.ArrayList;
77import java.util.Arrays;
78import java.util.Collections;
79import java.util.HashMap;
80import java.util.Iterator;
81import java.util.LinkedHashMap;
82import java.util.List;
83import java.util.Map;
84import java.util.Set;
85import java.util.concurrent.atomic.AtomicInteger;
86import java.util.regex.Matcher;
87import java.util.regex.Pattern;
88
89public class NotificationService {
90
91 public static final Object CATCHUP_LOCK = new Object();
92
93 private static final int LED_COLOR = 0xff00ff00;
94
95 private static final long[] CALL_PATTERN = {0, 500, 300, 600, 3000};
96
97 private static final String MESSAGES_GROUP = "eu.siacs.conversations.messages";
98 private static final String MISSED_CALLS_GROUP = "eu.siacs.conversations.missed_calls";
99 private static final int NOTIFICATION_ID_MULTIPLIER = 1024 * 1024;
100 static final int FOREGROUND_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 4;
101 private static final int NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 2;
102 private static final int ERROR_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 6;
103 private static final int INCOMING_CALL_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 8;
104 public static final int ONGOING_CALL_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 10;
105 public static final int MISSED_CALL_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 12;
106 private static final int DELIVERY_FAILED_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 13;
107 public static final int ONGOING_VIDEO_TRANSCODING_NOTIFICATION_ID =
108 NOTIFICATION_ID_MULTIPLIER * 14;
109 private final XmppConnectionService mXmppConnectionService;
110 private final LinkedHashMap<String, ArrayList<Message>> notifications = new LinkedHashMap<>();
111 private final HashMap<Conversation, AtomicInteger> mBacklogMessageCounter = new HashMap<>();
112 private final LinkedHashMap<Conversational, MissedCallsInfo> mMissedCalls =
113 new LinkedHashMap<>();
114 private Conversation mOpenConversation;
115 private boolean mIsInForeground;
116 private long mLastNotification;
117
118 private static final String INCOMING_CALLS_NOTIFICATION_CHANNEL = "incoming_calls_channel";
119 private static final String INCOMING_CALLS_NOTIFICATION_CHANNEL_PREFIX =
120 "incoming_calls_channel#";
121 private static final String MESSAGES_NOTIFICATION_CHANNEL = "messages";
122
123 NotificationService(final XmppConnectionService service) {
124 this.mXmppConnectionService = service;
125 }
126
127 private static boolean displaySnoozeAction(List<Message> messages) {
128 int numberOfMessagesWithoutReply = 0;
129 for (Message message : messages) {
130 if (message.getStatus() == Message.STATUS_RECEIVED) {
131 ++numberOfMessagesWithoutReply;
132 } else {
133 return false;
134 }
135 }
136 return numberOfMessagesWithoutReply >= 3;
137 }
138
139 public static Pattern generateNickHighlightPattern(final String nick) {
140 return Pattern.compile("(?<=(^|\\s))" + Pattern.quote(nick) + "(?=\\s|$|\\p{Punct})");
141 }
142
143 private static boolean isImageMessage(Message message) {
144 return message.getType() != Message.TYPE_TEXT
145 && message.getTransferable() == null
146 && !message.isDeleted()
147 && message.getEncryption() != Message.ENCRYPTION_PGP
148 && message.getFileParams().height > 0;
149 }
150
151 @RequiresApi(api = Build.VERSION_CODES.O)
152 void initializeChannels() {
153 final Context c = mXmppConnectionService;
154 final NotificationManager notificationManager =
155 c.getSystemService(NotificationManager.class);
156 if (notificationManager == null) {
157 return;
158 }
159
160 notificationManager.deleteNotificationChannel("export");
161 notificationManager.deleteNotificationChannel("incoming_calls");
162 notificationManager.deleteNotificationChannel(INCOMING_CALLS_NOTIFICATION_CHANNEL);
163
164 notificationManager.createNotificationChannelGroup(
165 new NotificationChannelGroup(
166 "status", c.getString(R.string.notification_group_status_information)));
167 notificationManager.createNotificationChannelGroup(
168 new NotificationChannelGroup(
169 "chats", c.getString(R.string.notification_group_messages)));
170 notificationManager.createNotificationChannelGroup(
171 new NotificationChannelGroup(
172 "calls", c.getString(R.string.notification_group_calls)));
173 final NotificationChannel foregroundServiceChannel =
174 new NotificationChannel(
175 "foreground",
176 c.getString(R.string.foreground_service_channel_name),
177 NotificationManager.IMPORTANCE_MIN);
178 foregroundServiceChannel.setDescription(
179 c.getString(
180 R.string.foreground_service_channel_description,
181 c.getString(R.string.app_name)));
182 foregroundServiceChannel.setShowBadge(false);
183 foregroundServiceChannel.setGroup("status");
184 notificationManager.createNotificationChannel(foregroundServiceChannel);
185 final NotificationChannel errorChannel =
186 new NotificationChannel(
187 "error",
188 c.getString(R.string.error_channel_name),
189 NotificationManager.IMPORTANCE_LOW);
190 errorChannel.setDescription(c.getString(R.string.error_channel_description));
191 errorChannel.setShowBadge(false);
192 errorChannel.setGroup("status");
193 notificationManager.createNotificationChannel(errorChannel);
194
195 final NotificationChannel videoCompressionChannel =
196 new NotificationChannel(
197 "compression",
198 c.getString(R.string.video_compression_channel_name),
199 NotificationManager.IMPORTANCE_LOW);
200 videoCompressionChannel.setShowBadge(false);
201 videoCompressionChannel.setGroup("status");
202 notificationManager.createNotificationChannel(videoCompressionChannel);
203
204 final NotificationChannel exportChannel =
205 new NotificationChannel(
206 "backup",
207 c.getString(R.string.backup_channel_name),
208 NotificationManager.IMPORTANCE_LOW);
209 exportChannel.setShowBadge(false);
210 exportChannel.setGroup("status");
211 notificationManager.createNotificationChannel(exportChannel);
212
213 createInitialIncomingCallChannelIfNecessary(c);
214
215 final NotificationChannel ongoingCallsChannel =
216 new NotificationChannel(
217 "ongoing_calls",
218 c.getString(R.string.ongoing_calls_channel_name),
219 NotificationManager.IMPORTANCE_LOW);
220 ongoingCallsChannel.setShowBadge(false);
221 ongoingCallsChannel.setGroup("calls");
222 notificationManager.createNotificationChannel(ongoingCallsChannel);
223
224 final NotificationChannel missedCallsChannel =
225 new NotificationChannel(
226 "missed_calls",
227 c.getString(R.string.missed_calls_channel_name),
228 NotificationManager.IMPORTANCE_HIGH);
229 missedCallsChannel.setShowBadge(true);
230 missedCallsChannel.setSound(null, null);
231 missedCallsChannel.setLightColor(LED_COLOR);
232 missedCallsChannel.enableLights(true);
233 missedCallsChannel.setGroup("calls");
234 notificationManager.createNotificationChannel(missedCallsChannel);
235
236 final NotificationChannel messagesChannel =
237 new NotificationChannel(
238 MESSAGES_NOTIFICATION_CHANNEL,
239 c.getString(R.string.messages_channel_name),
240 NotificationManager.IMPORTANCE_HIGH);
241 messagesChannel.setShowBadge(true);
242 messagesChannel.setSound(
243 RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION),
244 new AudioAttributes.Builder()
245 .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
246 .setUsage(AudioAttributes.USAGE_NOTIFICATION)
247 .build());
248 messagesChannel.setLightColor(LED_COLOR);
249 final int dat = 70;
250 final long[] pattern = {0, 3 * dat, dat, dat};
251 messagesChannel.setVibrationPattern(pattern);
252 messagesChannel.enableVibration(true);
253 messagesChannel.enableLights(true);
254 messagesChannel.setGroup("chats");
255 notificationManager.createNotificationChannel(messagesChannel);
256 final NotificationChannel silentMessagesChannel =
257 new NotificationChannel(
258 "silent_messages",
259 c.getString(R.string.silent_messages_channel_name),
260 NotificationManager.IMPORTANCE_LOW);
261 silentMessagesChannel.setDescription(
262 c.getString(R.string.silent_messages_channel_description));
263 silentMessagesChannel.setShowBadge(true);
264 silentMessagesChannel.setLightColor(LED_COLOR);
265 silentMessagesChannel.enableLights(true);
266 silentMessagesChannel.setGroup("chats");
267 notificationManager.createNotificationChannel(silentMessagesChannel);
268
269 final NotificationChannel deliveryFailedChannel =
270 new NotificationChannel(
271 "delivery_failed",
272 c.getString(R.string.delivery_failed_channel_name),
273 NotificationManager.IMPORTANCE_DEFAULT);
274 deliveryFailedChannel.setShowBadge(false);
275 deliveryFailedChannel.setSound(
276 RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION),
277 new AudioAttributes.Builder()
278 .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
279 .setUsage(AudioAttributes.USAGE_NOTIFICATION)
280 .build());
281 deliveryFailedChannel.setGroup("chats");
282 notificationManager.createNotificationChannel(deliveryFailedChannel);
283 }
284
285 @RequiresApi(api = Build.VERSION_CODES.O)
286 private static void createInitialIncomingCallChannelIfNecessary(final Context context) {
287 final var currentIteration = getCurrentIncomingCallChannelIteration(context);
288 if (currentIteration.isPresent()) {
289 return;
290 }
291 createInitialIncomingCallChannel(context);
292 }
293
294 @RequiresApi(api = Build.VERSION_CODES.O)
295 public static Optional<Integer> getCurrentIncomingCallChannelIteration(final Context context) {
296 final var notificationManager = context.getSystemService(NotificationManager.class);
297 for (final NotificationChannel channel : notificationManager.getNotificationChannels()) {
298 final String id = channel.getId();
299 if (Strings.isNullOrEmpty(id)) {
300 continue;
301 }
302 if (id.startsWith(INCOMING_CALLS_NOTIFICATION_CHANNEL_PREFIX)) {
303 final var parts = Splitter.on('#').splitToList(id);
304 if (parts.size() == 2) {
305 final var iteration = Ints.tryParse(parts.get(1));
306 if (iteration != null) {
307 return Optional.of(iteration);
308 }
309 }
310 }
311 }
312 return Optional.absent();
313 }
314
315 @RequiresApi(api = Build.VERSION_CODES.O)
316 public static Optional<NotificationChannel> getCurrentIncomingCallChannel(
317 final Context context) {
318 final var iteration = getCurrentIncomingCallChannelIteration(context);
319 return iteration.transform(
320 i -> {
321 final var notificationManager =
322 context.getSystemService(NotificationManager.class);
323 return notificationManager.getNotificationChannel(
324 INCOMING_CALLS_NOTIFICATION_CHANNEL_PREFIX + i);
325 });
326 }
327
328 @RequiresApi(api = Build.VERSION_CODES.O)
329 private static void createInitialIncomingCallChannel(final Context context) {
330 final var appSettings = new AppSettings(context);
331 final var ringtoneUri = appSettings.getRingtone();
332 createIncomingCallChannel(context, ringtoneUri, 0);
333 }
334
335 @RequiresApi(api = Build.VERSION_CODES.O)
336 public static void recreateIncomingCallChannel(final Context context, final Uri ringtone) {
337 final var currentIteration = getCurrentIncomingCallChannelIteration(context);
338 final int nextIteration;
339 if (currentIteration.isPresent()) {
340 final var notificationManager = context.getSystemService(NotificationManager.class);
341 notificationManager.deleteNotificationChannel(
342 INCOMING_CALLS_NOTIFICATION_CHANNEL_PREFIX + currentIteration.get());
343 nextIteration = currentIteration.get() + 1;
344 } else {
345 nextIteration = 0;
346 }
347 createIncomingCallChannel(context, ringtone, nextIteration);
348 }
349
350 @RequiresApi(api = Build.VERSION_CODES.O)
351 private static void createIncomingCallChannel(
352 final Context context, final Uri ringtoneUri, final int iteration) {
353 final var notificationManager = context.getSystemService(NotificationManager.class);
354 final var id = INCOMING_CALLS_NOTIFICATION_CHANNEL_PREFIX + iteration;
355 Log.d(Config.LOGTAG, "creating incoming call channel with id " + id);
356 final NotificationChannel incomingCallsChannel =
357 new NotificationChannel(
358 id,
359 context.getString(R.string.incoming_calls_channel_name),
360 NotificationManager.IMPORTANCE_HIGH);
361 incomingCallsChannel.setSound(
362 ringtoneUri,
363 new AudioAttributes.Builder()
364 .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
365 .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
366 .build());
367 incomingCallsChannel.setShowBadge(false);
368 incomingCallsChannel.setLightColor(LED_COLOR);
369 incomingCallsChannel.enableLights(true);
370 incomingCallsChannel.setGroup("calls");
371 incomingCallsChannel.setBypassDnd(true);
372 incomingCallsChannel.enableVibration(true);
373 incomingCallsChannel.setVibrationPattern(CALL_PATTERN);
374 notificationManager.createNotificationChannel(incomingCallsChannel);
375 }
376
377 private boolean notifyMessage(final Message message) {
378 final Conversation conversation = (Conversation) message.getConversation();
379 return message.getStatus() == Message.STATUS_RECEIVED
380 && !conversation.isMuted()
381 && (conversation.alwaysNotify() || wasHighlightedOrPrivate(message))
382 && (!conversation.isWithStranger() || notificationsFromStrangers())
383 && message.getType() != Message.TYPE_RTP_SESSION;
384 }
385
386 private boolean notifyMissedCall(final Message message) {
387 return message.getType() == Message.TYPE_RTP_SESSION
388 && message.getStatus() == Message.STATUS_RECEIVED;
389 }
390
391 public boolean notificationsFromStrangers() {
392 return mXmppConnectionService.getBooleanPreference(
393 "notifications_from_strangers", R.bool.notifications_from_strangers);
394 }
395
396 public void pushFromBacklog(final Message message) {
397 if (notifyMessage(message)) {
398 synchronized (notifications) {
399 getBacklogMessageCounter((Conversation) message.getConversation())
400 .incrementAndGet();
401 pushToStack(message);
402 }
403 } else if (notifyMissedCall(message)) {
404 synchronized (mMissedCalls) {
405 pushMissedCall(message);
406 }
407 }
408 }
409
410 private AtomicInteger getBacklogMessageCounter(Conversation conversation) {
411 synchronized (mBacklogMessageCounter) {
412 if (!mBacklogMessageCounter.containsKey(conversation)) {
413 mBacklogMessageCounter.put(conversation, new AtomicInteger(0));
414 }
415 return mBacklogMessageCounter.get(conversation);
416 }
417 }
418
419 void pushFromDirectReply(final Message message) {
420 synchronized (notifications) {
421 pushToStack(message);
422 updateNotification(false);
423 }
424 }
425
426 public void finishBacklog(boolean notify, Account account) {
427 synchronized (notifications) {
428 mXmppConnectionService.updateUnreadCountBadge();
429 if (account == null || !notify) {
430 updateNotification(notify);
431 } else {
432 final int count;
433 final List<String> conversations;
434 synchronized (this.mBacklogMessageCounter) {
435 conversations = getBacklogConversations(account);
436 count = getBacklogMessageCount(account);
437 }
438 updateNotification(count > 0, conversations);
439 }
440 }
441 synchronized (mMissedCalls) {
442 updateMissedCallNotifications(mMissedCalls.keySet());
443 }
444 }
445
446 private List<String> getBacklogConversations(Account account) {
447 final List<String> conversations = new ArrayList<>();
448 for (Map.Entry<Conversation, AtomicInteger> entry : mBacklogMessageCounter.entrySet()) {
449 if (entry.getKey().getAccount() == account) {
450 conversations.add(entry.getKey().getUuid());
451 }
452 }
453 return conversations;
454 }
455
456 private int getBacklogMessageCount(Account account) {
457 int count = 0;
458 for (Iterator<Map.Entry<Conversation, AtomicInteger>> it =
459 mBacklogMessageCounter.entrySet().iterator();
460 it.hasNext(); ) {
461 Map.Entry<Conversation, AtomicInteger> entry = it.next();
462 if (entry.getKey().getAccount() == account) {
463 count += entry.getValue().get();
464 it.remove();
465 }
466 }
467 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": backlog message count=" + count);
468 return count;
469 }
470
471 void finishBacklog() {
472 finishBacklog(false, null);
473 }
474
475 private void pushToStack(final Message message) {
476 final String conversationUuid = message.getConversationUuid();
477 if (notifications.containsKey(conversationUuid)) {
478 notifications.get(conversationUuid).add(message);
479 } else {
480 final ArrayList<Message> mList = new ArrayList<>();
481 mList.add(message);
482 notifications.put(conversationUuid, mList);
483 }
484 }
485
486 public void push(final Message message) {
487 synchronized (CATCHUP_LOCK) {
488 final XmppConnection connection =
489 message.getConversation().getAccount().getXmppConnection();
490 if (connection != null && connection.isWaitingForSmCatchup()) {
491 connection.incrementSmCatchupMessageCounter();
492 pushFromBacklog(message);
493 } else {
494 pushNow(message);
495 }
496 }
497 }
498
499 public void pushFailedDelivery(final Message message) {
500 final Conversation conversation = (Conversation) message.getConversation();
501 final boolean isScreenLocked = !mXmppConnectionService.isScreenLocked();
502 if (this.mIsInForeground
503 && isScreenLocked
504 && this.mOpenConversation == message.getConversation()) {
505 Log.d(
506 Config.LOGTAG,
507 message.getConversation().getAccount().getJid().asBareJid()
508 + ": suppressing failed delivery notification because conversation is open");
509 return;
510 }
511 final PendingIntent pendingIntent = createContentIntent(conversation);
512 final int notificationId =
513 generateRequestCode(conversation, 0) + DELIVERY_FAILED_NOTIFICATION_ID;
514 final int failedDeliveries = conversation.countFailedDeliveries();
515 final Notification notification =
516 new Builder(mXmppConnectionService, "delivery_failed")
517 .setContentTitle(conversation.getName())
518 .setAutoCancel(true)
519 .setSmallIcon(R.drawable.ic_error_24dp)
520 .setContentText(
521 mXmppConnectionService
522 .getResources()
523 .getQuantityText(
524 R.plurals.some_messages_could_not_be_delivered,
525 failedDeliveries))
526 .setGroup("delivery_failed")
527 .setContentIntent(pendingIntent)
528 .build();
529 final Notification summaryNotification =
530 new Builder(mXmppConnectionService, "delivery_failed")
531 .setContentTitle(
532 mXmppConnectionService.getString(R.string.failed_deliveries))
533 .setContentText(
534 mXmppConnectionService
535 .getResources()
536 .getQuantityText(
537 R.plurals.some_messages_could_not_be_delivered,
538 1024))
539 .setSmallIcon(R.drawable.ic_error_24dp)
540 .setGroup("delivery_failed")
541 .setGroupSummary(true)
542 .setAutoCancel(true)
543 .build();
544 notify(notificationId, notification);
545 notify(DELIVERY_FAILED_NOTIFICATION_ID, summaryNotification);
546 }
547
548 public synchronized void startRinging(
549 final AbstractJingleConnection.Id id, final Set<Media> media) {
550 showIncomingCallNotification(id, media, false);
551 }
552
553 private void showIncomingCallNotification(
554 final AbstractJingleConnection.Id id,
555 final Set<Media> media,
556 final boolean onlyAlertOnce) {
557 final Intent fullScreenIntent =
558 new Intent(mXmppConnectionService, RtpSessionActivity.class);
559 fullScreenIntent.putExtra(
560 RtpSessionActivity.EXTRA_ACCOUNT,
561 id.account.getJid().asBareJid().toEscapedString());
562 fullScreenIntent.putExtra(RtpSessionActivity.EXTRA_WITH, id.with.toEscapedString());
563 fullScreenIntent.putExtra(RtpSessionActivity.EXTRA_SESSION_ID, id.sessionId);
564 fullScreenIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
565 fullScreenIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
566 final int channelIteration;
567 if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
568 channelIteration = getCurrentIncomingCallChannelIteration(mXmppConnectionService).or(0);
569 } else {
570 channelIteration = 0;
571 }
572 final var channelId = INCOMING_CALLS_NOTIFICATION_CHANNEL_PREFIX + channelIteration;
573 Log.d(
574 Config.LOGTAG,
575 "showing incoming call notification on channel "
576 + channelId
577 + ", onlyAlertOnce="
578 + onlyAlertOnce);
579 final NotificationCompat.Builder builder =
580 new NotificationCompat.Builder(mXmppConnectionService, channelId);
581 if (media.contains(Media.VIDEO)) {
582 builder.setSmallIcon(R.drawable.ic_videocam_24dp);
583 builder.setContentTitle(
584 mXmppConnectionService.getString(R.string.rtp_state_incoming_video_call));
585 } else {
586 builder.setSmallIcon(R.drawable.ic_call_24dp);
587 builder.setContentTitle(
588 mXmppConnectionService.getString(R.string.rtp_state_incoming_call));
589 }
590 final Contact contact = id.getContact();
591 builder.setLargeIcon(
592 mXmppConnectionService
593 .getAvatarService()
594 .get(contact, AvatarService.getSystemUiAvatarSize(mXmppConnectionService)));
595 final Uri systemAccount = contact.getSystemAccount();
596 if (systemAccount != null) {
597 builder.addPerson(systemAccount.toString());
598 }
599 if (!onlyAlertOnce) {
600 final var appSettings = new AppSettings(mXmppConnectionService);
601 final var ringtone = appSettings.getRingtone();
602 if (ringtone != null) {
603 builder.setSound(ringtone, AudioManager.STREAM_RING);
604 }
605 builder.setVibrate(CALL_PATTERN);
606 }
607 builder.setOnlyAlertOnce(onlyAlertOnce);
608 builder.setContentText(id.account.getRoster().getContact(id.with).getDisplayName());
609 builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC);
610 builder.setPriority(NotificationCompat.PRIORITY_HIGH);
611 builder.setCategory(NotificationCompat.CATEGORY_CALL);
612 final PendingIntent pendingIntent = createPendingRtpSession(id, Intent.ACTION_VIEW, 101);
613 builder.setFullScreenIntent(pendingIntent, true);
614 builder.setContentIntent(pendingIntent); // old androids need this?
615 builder.setOngoing(true);
616 builder.addAction(
617 new NotificationCompat.Action.Builder(
618 R.drawable.ic_call_end_24dp,
619 mXmppConnectionService.getString(R.string.dismiss_call),
620 createCallAction(
621 id.sessionId,
622 XmppConnectionService.ACTION_DISMISS_CALL,
623 102))
624 .build());
625 builder.addAction(
626 new NotificationCompat.Action.Builder(
627 R.drawable.ic_call_24dp,
628 mXmppConnectionService.getString(R.string.answer_call),
629 createPendingRtpSession(
630 id, RtpSessionActivity.ACTION_ACCEPT_CALL, 103))
631 .build());
632 modifyIncomingCall(builder);
633 final Notification notification = builder.build();
634 notification.audioAttributes = new AudioAttributes.Builder()
635 .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
636 .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
637 .build();
638 notification.flags = notification.flags | Notification.FLAG_INSISTENT;
639 notify(INCOMING_CALL_NOTIFICATION_ID, notification);
640 }
641
642 public Notification getOngoingCallNotification(
643 final XmppConnectionService.OngoingCall ongoingCall) {
644 final AbstractJingleConnection.Id id = ongoingCall.id;
645 final NotificationCompat.Builder builder =
646 new NotificationCompat.Builder(mXmppConnectionService, "ongoing_calls");
647 if (ongoingCall.media.contains(Media.VIDEO)) {
648 builder.setSmallIcon(R.drawable.ic_videocam_24dp);
649 if (ongoingCall.reconnecting) {
650 builder.setContentTitle(
651 mXmppConnectionService.getString(R.string.reconnecting_video_call));
652 } else {
653 builder.setContentTitle(
654 mXmppConnectionService.getString(R.string.ongoing_video_call));
655 }
656 } else {
657 builder.setSmallIcon(R.drawable.ic_call_24dp);
658 if (ongoingCall.reconnecting) {
659 builder.setContentTitle(
660 mXmppConnectionService.getString(R.string.reconnecting_call));
661 } else {
662 builder.setContentTitle(mXmppConnectionService.getString(R.string.ongoing_call));
663 }
664 }
665 builder.setContentText(id.account.getRoster().getContact(id.with).getDisplayName());
666 builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC);
667 builder.setPriority(NotificationCompat.PRIORITY_HIGH);
668 builder.setCategory(NotificationCompat.CATEGORY_CALL);
669 builder.setContentIntent(createPendingRtpSession(id, Intent.ACTION_VIEW, 101));
670 builder.setOngoing(true);
671 builder.addAction(
672 new NotificationCompat.Action.Builder(
673 R.drawable.ic_call_end_24dp,
674 mXmppConnectionService.getString(R.string.hang_up),
675 createCallAction(
676 id.sessionId, XmppConnectionService.ACTION_END_CALL, 104))
677 .build());
678 builder.setLocalOnly(true);
679 return builder.build();
680 }
681
682 private PendingIntent createPendingRtpSession(
683 final AbstractJingleConnection.Id id, final String action, final int requestCode) {
684 final Intent fullScreenIntent =
685 new Intent(mXmppConnectionService, RtpSessionActivity.class);
686 fullScreenIntent.setAction(action);
687 fullScreenIntent.putExtra(
688 RtpSessionActivity.EXTRA_ACCOUNT,
689 id.account.getJid().asBareJid().toEscapedString());
690 fullScreenIntent.putExtra(RtpSessionActivity.EXTRA_WITH, id.with.toEscapedString());
691 fullScreenIntent.putExtra(RtpSessionActivity.EXTRA_SESSION_ID, id.sessionId);
692 return PendingIntent.getActivity(
693 mXmppConnectionService,
694 requestCode,
695 fullScreenIntent,
696 s()
697 ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
698 : PendingIntent.FLAG_UPDATE_CURRENT);
699 }
700
701 public void cancelIncomingCallNotification() {
702 cancel(INCOMING_CALL_NOTIFICATION_ID);
703 }
704
705 public boolean stopSoundAndVibration() {
706 final var jingleRtpConnection =
707 mXmppConnectionService.getJingleConnectionManager().getOngoingRtpConnection();
708 if (jingleRtpConnection == null) {
709 return false;
710 }
711 final var notificationManager = mXmppConnectionService.getSystemService(NotificationManager.class);
712 if (Iterables.any(
713 Arrays.asList(notificationManager.getActiveNotifications()),
714 n -> n.getId() == INCOMING_CALL_NOTIFICATION_ID)) {
715 Log.d(Config.LOGTAG, "stopping sound and vibration for incoming call notification");
716 showIncomingCallNotification(
717 jingleRtpConnection.getId(), jingleRtpConnection.getMedia(), true);
718 return true;
719 }
720 return false;
721 }
722
723 public static void cancelIncomingCallNotification(final Context context) {
724 final NotificationManagerCompat notificationManager =
725 NotificationManagerCompat.from(context);
726 try {
727 notificationManager.cancel(INCOMING_CALL_NOTIFICATION_ID);
728 } catch (RuntimeException e) {
729 Log.d(Config.LOGTAG, "unable to cancel incoming call notification after crash", e);
730 }
731 }
732
733 private void pushNow(final Message message) {
734 mXmppConnectionService.updateUnreadCountBadge();
735 if (!notifyMessage(message)) {
736 Log.d(
737 Config.LOGTAG,
738 message.getConversation().getAccount().getJid().asBareJid()
739 + ": suppressing notification because turned off");
740 return;
741 }
742 final boolean isScreenLocked = mXmppConnectionService.isScreenLocked();
743 if (this.mIsInForeground
744 && !isScreenLocked
745 && this.mOpenConversation == message.getConversation()) {
746 Log.d(
747 Config.LOGTAG,
748 message.getConversation().getAccount().getJid().asBareJid()
749 + ": suppressing notification because conversation is open");
750 return;
751 }
752 synchronized (notifications) {
753 pushToStack(message);
754 final Conversational conversation = message.getConversation();
755 final Account account = conversation.getAccount();
756 final boolean doNotify =
757 (!(this.mIsInForeground && this.mOpenConversation == null) || isScreenLocked)
758 && !account.inGracePeriod()
759 && !this.inMiniGracePeriod(account);
760 updateNotification(doNotify, Collections.singletonList(conversation.getUuid()));
761 }
762 }
763
764 private void pushMissedCall(final Message message) {
765 final Conversational conversation = message.getConversation();
766 final MissedCallsInfo info = mMissedCalls.get(conversation);
767 if (info == null) {
768 mMissedCalls.put(conversation, new MissedCallsInfo(message.getTimeSent()));
769 } else {
770 info.newMissedCall(message.getTimeSent());
771 }
772 }
773
774 public void pushMissedCallNow(final Message message) {
775 synchronized (mMissedCalls) {
776 pushMissedCall(message);
777 updateMissedCallNotifications(Collections.singleton(message.getConversation()));
778 }
779 }
780
781 public void clear(final Conversation conversation) {
782 clearMessages(conversation);
783 clearMissedCalls(conversation);
784 }
785
786 public void clearMessages() {
787 synchronized (notifications) {
788 for (ArrayList<Message> messages : notifications.values()) {
789 markAsReadIfHasDirectReply(messages);
790 }
791 notifications.clear();
792 updateNotification(false);
793 }
794 }
795
796 public void clearMessages(final Conversation conversation) {
797 synchronized (this.mBacklogMessageCounter) {
798 this.mBacklogMessageCounter.remove(conversation);
799 }
800 synchronized (notifications) {
801 markAsReadIfHasDirectReply(conversation);
802 if (notifications.remove(conversation.getUuid()) != null) {
803 cancel(conversation.getUuid(), NOTIFICATION_ID);
804 updateNotification(false, null, true);
805 }
806 }
807 }
808
809 public void clearMissedCall(final Message message) {
810 synchronized (mMissedCalls) {
811 final Iterator<Map.Entry<Conversational, MissedCallsInfo>> iterator =
812 mMissedCalls.entrySet().iterator();
813 while (iterator.hasNext()) {
814 final Map.Entry<Conversational, MissedCallsInfo> entry = iterator.next();
815 final Conversational conversational = entry.getKey();
816 final MissedCallsInfo missedCallsInfo = entry.getValue();
817 if (conversational.getUuid().equals(message.getConversation().getUuid())) {
818 if (missedCallsInfo.removeMissedCall()) {
819 cancel(conversational.getUuid(), MISSED_CALL_NOTIFICATION_ID);
820 Log.d(
821 Config.LOGTAG,
822 conversational.getAccount().getJid().asBareJid()
823 + ": dismissed missed call because call was picked up on 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 = mXmppConnectionService.getShortcutService().getShortcutInfoCompat(contact);
1349 } else {
1350 info =
1351 mXmppConnectionService
1352 .getShortcutService()
1353 .getShortcutInfoCompat(conversation.getMucOptions());
1354 }
1355 notificationBuilder.setWhen(conversation.getLatestMessage().getTimeSent());
1356 notificationBuilder.setSmallIcon(R.drawable.ic_app_icon_notification);
1357 notificationBuilder.setDeleteIntent(createDeleteIntent(conversation));
1358 notificationBuilder.setContentIntent(createContentIntent(conversation));
1359 if (channel.equals(MESSAGES_NOTIFICATION_CHANNEL)) {
1360 // when do not want 'customized' notifications for silent notifications in their
1361 // respective channels
1362 notificationBuilder.setShortcutInfo(info);
1363 if (Build.VERSION.SDK_INT >= 30) {
1364 mXmppConnectionService
1365 .getSystemService(ShortcutManager.class)
1366 .pushDynamicShortcut(info.toShortcutInfo());
1367 }
1368 }
1369 return notificationBuilder;
1370 }
1371
1372 private void modifyForImage(
1373 final Builder builder, final Message message, final ArrayList<Message> messages) {
1374 try {
1375 final Bitmap bitmap =
1376 mXmppConnectionService
1377 .getFileBackend()
1378 .getThumbnail(message, getPixel(288), false);
1379 final ArrayList<Message> tmp = new ArrayList<>();
1380 for (final Message msg : messages) {
1381 if (msg.getType() == Message.TYPE_TEXT && msg.getTransferable() == null) {
1382 tmp.add(msg);
1383 }
1384 }
1385 final BigPictureStyle bigPictureStyle = new NotificationCompat.BigPictureStyle();
1386 bigPictureStyle.bigPicture(bitmap);
1387 if (tmp.size() > 0) {
1388 CharSequence text = getMergedBodies(tmp);
1389 bigPictureStyle.setSummaryText(text);
1390 builder.setContentText(text);
1391 builder.setTicker(text);
1392 } else {
1393 final String description =
1394 UIHelper.getFileDescriptionString(mXmppConnectionService, message);
1395 builder.setContentText(description);
1396 builder.setTicker(description);
1397 }
1398 builder.setStyle(bigPictureStyle);
1399 } catch (final IOException e) {
1400 modifyForTextOnly(builder, messages);
1401 }
1402 }
1403
1404 private Person getPerson(Message message) {
1405 final Contact contact = message.getContact();
1406 final Person.Builder builder = new Person.Builder();
1407 if (contact != null) {
1408 builder.setName(contact.getDisplayName());
1409 final Uri uri = contact.getSystemAccount();
1410 if (uri != null) {
1411 builder.setUri(uri.toString());
1412 }
1413 } else {
1414 builder.setName(UIHelper.getMessageDisplayName(message));
1415 }
1416 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
1417 builder.setIcon(
1418 IconCompat.createWithBitmap(
1419 mXmppConnectionService
1420 .getAvatarService()
1421 .get(
1422 message,
1423 AvatarService.getSystemUiAvatarSize(
1424 mXmppConnectionService),
1425 false)));
1426 }
1427 return builder.build();
1428 }
1429
1430 private void modifyForTextOnly(final Builder builder, final ArrayList<Message> messages) {
1431 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
1432 final Conversation conversation = (Conversation) messages.get(0).getConversation();
1433 final Person.Builder meBuilder =
1434 new Person.Builder().setName(mXmppConnectionService.getString(R.string.me));
1435 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
1436 meBuilder.setIcon(
1437 IconCompat.createWithBitmap(
1438 mXmppConnectionService
1439 .getAvatarService()
1440 .get(
1441 conversation.getAccount(),
1442 AvatarService.getSystemUiAvatarSize(
1443 mXmppConnectionService))));
1444 }
1445 final Person me = meBuilder.build();
1446 NotificationCompat.MessagingStyle messagingStyle =
1447 new NotificationCompat.MessagingStyle(me);
1448 final boolean multiple = conversation.getMode() == Conversation.MODE_MULTI;
1449 if (multiple) {
1450 messagingStyle.setConversationTitle(conversation.getName());
1451 }
1452 for (Message message : messages) {
1453 final Person sender =
1454 message.getStatus() == Message.STATUS_RECEIVED ? getPerson(message) : null;
1455 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && isImageMessage(message)) {
1456 final Uri dataUri =
1457 FileBackend.getMediaUri(
1458 mXmppConnectionService,
1459 mXmppConnectionService.getFileBackend().getFile(message));
1460 NotificationCompat.MessagingStyle.Message imageMessage =
1461 new NotificationCompat.MessagingStyle.Message(
1462 UIHelper.getMessagePreview(mXmppConnectionService, message)
1463 .first,
1464 message.getTimeSent(),
1465 sender);
1466 if (dataUri != null) {
1467 imageMessage.setData(message.getMimeType(), dataUri);
1468 }
1469 messagingStyle.addMessage(imageMessage);
1470 } else {
1471 messagingStyle.addMessage(
1472 UIHelper.getMessagePreview(mXmppConnectionService, message).first,
1473 message.getTimeSent(),
1474 sender);
1475 }
1476 }
1477 messagingStyle.setGroupConversation(multiple);
1478 builder.setStyle(messagingStyle);
1479 } else {
1480 if (messages.get(0).getConversation().getMode() == Conversation.MODE_SINGLE) {
1481 builder.setStyle(
1482 new NotificationCompat.BigTextStyle().bigText(getMergedBodies(messages)));
1483 final CharSequence preview =
1484 UIHelper.getMessagePreview(
1485 mXmppConnectionService, messages.get(messages.size() - 1))
1486 .first;
1487 builder.setContentText(preview);
1488 builder.setTicker(preview);
1489 builder.setNumber(messages.size());
1490 } else {
1491 final NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle();
1492 SpannableString styledString;
1493 for (Message message : messages) {
1494 final String name = UIHelper.getMessageDisplayName(message);
1495 styledString = new SpannableString(name + ": " + message.getBody());
1496 styledString.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), 0);
1497 style.addLine(styledString);
1498 }
1499 builder.setStyle(style);
1500 int count = messages.size();
1501 if (count == 1) {
1502 final String name = UIHelper.getMessageDisplayName(messages.get(0));
1503 styledString = new SpannableString(name + ": " + messages.get(0).getBody());
1504 styledString.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), 0);
1505 builder.setContentText(styledString);
1506 builder.setTicker(styledString);
1507 } else {
1508 final String text =
1509 mXmppConnectionService
1510 .getResources()
1511 .getQuantityString(R.plurals.x_messages, count, count);
1512 builder.setContentText(text);
1513 builder.setTicker(text);
1514 }
1515 }
1516 }
1517 }
1518
1519 private Message getImage(final Iterable<Message> messages) {
1520 Message image = null;
1521 for (final Message message : messages) {
1522 if (message.getStatus() != Message.STATUS_RECEIVED) {
1523 return null;
1524 }
1525 if (isImageMessage(message)) {
1526 image = message;
1527 }
1528 }
1529 return image;
1530 }
1531
1532 private Message getFirstDownloadableMessage(final Iterable<Message> messages) {
1533 for (final Message message : messages) {
1534 if (message.getTransferable() != null
1535 || (message.getType() == Message.TYPE_TEXT && message.treatAsDownloadable())) {
1536 return message;
1537 }
1538 }
1539 return null;
1540 }
1541
1542 private Message getFirstLocationMessage(final Iterable<Message> messages) {
1543 for (final Message message : messages) {
1544 if (message.isGeoUri()) {
1545 return message;
1546 }
1547 }
1548 return null;
1549 }
1550
1551 private CharSequence getMergedBodies(final ArrayList<Message> messages) {
1552 final StringBuilder text = new StringBuilder();
1553 for (Message message : messages) {
1554 if (text.length() != 0) {
1555 text.append("\n");
1556 }
1557 text.append(UIHelper.getMessagePreview(mXmppConnectionService, message).first);
1558 }
1559 return text.toString();
1560 }
1561
1562 private PendingIntent createShowLocationIntent(final Message message) {
1563 Iterable<Intent> intents =
1564 GeoHelper.createGeoIntentsFromMessage(mXmppConnectionService, message);
1565 for (final Intent intent : intents) {
1566 if (intent.resolveActivity(mXmppConnectionService.getPackageManager()) != null) {
1567 return PendingIntent.getActivity(
1568 mXmppConnectionService,
1569 generateRequestCode(message.getConversation(), 18),
1570 intent,
1571 s()
1572 ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1573 : PendingIntent.FLAG_UPDATE_CURRENT);
1574 }
1575 }
1576 return null;
1577 }
1578
1579 private PendingIntent createContentIntent(
1580 final String conversationUuid, final String downloadMessageUuid) {
1581 final Intent viewConversationIntent =
1582 new Intent(mXmppConnectionService, ConversationsActivity.class);
1583 viewConversationIntent.setAction(ConversationsActivity.ACTION_VIEW_CONVERSATION);
1584 viewConversationIntent.putExtra(ConversationsActivity.EXTRA_CONVERSATION, conversationUuid);
1585 if (downloadMessageUuid != null) {
1586 viewConversationIntent.putExtra(
1587 ConversationsActivity.EXTRA_DOWNLOAD_UUID, downloadMessageUuid);
1588 return PendingIntent.getActivity(
1589 mXmppConnectionService,
1590 generateRequestCode(conversationUuid, 8),
1591 viewConversationIntent,
1592 s()
1593 ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1594 : PendingIntent.FLAG_UPDATE_CURRENT);
1595 } else {
1596 return PendingIntent.getActivity(
1597 mXmppConnectionService,
1598 generateRequestCode(conversationUuid, 10),
1599 viewConversationIntent,
1600 s()
1601 ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1602 : PendingIntent.FLAG_UPDATE_CURRENT);
1603 }
1604 }
1605
1606 private int generateRequestCode(String uuid, int actionId) {
1607 return (actionId * NOTIFICATION_ID_MULTIPLIER)
1608 + (uuid.hashCode() % NOTIFICATION_ID_MULTIPLIER);
1609 }
1610
1611 private int generateRequestCode(Conversational conversation, int actionId) {
1612 return generateRequestCode(conversation.getUuid(), actionId);
1613 }
1614
1615 private PendingIntent createDownloadIntent(final Message message) {
1616 return createContentIntent(message.getConversationUuid(), message.getUuid());
1617 }
1618
1619 private PendingIntent createContentIntent(final Conversational conversation) {
1620 return createContentIntent(conversation.getUuid(), null);
1621 }
1622
1623 private PendingIntent createDeleteIntent(final Conversation conversation) {
1624 final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
1625 intent.setAction(XmppConnectionService.ACTION_CLEAR_MESSAGE_NOTIFICATION);
1626 if (conversation != null) {
1627 intent.putExtra("uuid", conversation.getUuid());
1628 return PendingIntent.getService(
1629 mXmppConnectionService,
1630 generateRequestCode(conversation, 20),
1631 intent,
1632 s()
1633 ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1634 : PendingIntent.FLAG_UPDATE_CURRENT);
1635 }
1636 return PendingIntent.getService(
1637 mXmppConnectionService,
1638 0,
1639 intent,
1640 s()
1641 ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1642 : PendingIntent.FLAG_UPDATE_CURRENT);
1643 }
1644
1645 private PendingIntent createMissedCallsDeleteIntent(final Conversational conversation) {
1646 final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
1647 intent.setAction(XmppConnectionService.ACTION_CLEAR_MISSED_CALL_NOTIFICATION);
1648 if (conversation != null) {
1649 intent.putExtra("uuid", conversation.getUuid());
1650 return PendingIntent.getService(
1651 mXmppConnectionService,
1652 generateRequestCode(conversation, 21),
1653 intent,
1654 s()
1655 ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1656 : PendingIntent.FLAG_UPDATE_CURRENT);
1657 }
1658 return PendingIntent.getService(
1659 mXmppConnectionService,
1660 1,
1661 intent,
1662 s()
1663 ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1664 : PendingIntent.FLAG_UPDATE_CURRENT);
1665 }
1666
1667 private PendingIntent createReplyIntent(
1668 final Conversation conversation,
1669 final String lastMessageUuid,
1670 final boolean dismissAfterReply) {
1671 final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
1672 intent.setAction(XmppConnectionService.ACTION_REPLY_TO_CONVERSATION);
1673 intent.putExtra("uuid", conversation.getUuid());
1674 intent.putExtra("dismiss_notification", dismissAfterReply);
1675 intent.putExtra("last_message_uuid", lastMessageUuid);
1676 final int id = generateRequestCode(conversation, dismissAfterReply ? 12 : 14);
1677 return PendingIntent.getService(
1678 mXmppConnectionService,
1679 id,
1680 intent,
1681 s()
1682 ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1683 : PendingIntent.FLAG_UPDATE_CURRENT);
1684 }
1685
1686 private PendingIntent createReadPendingIntent(Conversation conversation) {
1687 final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
1688 intent.setAction(XmppConnectionService.ACTION_MARK_AS_READ);
1689 intent.putExtra("uuid", conversation.getUuid());
1690 intent.setPackage(mXmppConnectionService.getPackageName());
1691 return PendingIntent.getService(
1692 mXmppConnectionService,
1693 generateRequestCode(conversation, 16),
1694 intent,
1695 s()
1696 ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1697 : PendingIntent.FLAG_UPDATE_CURRENT);
1698 }
1699
1700 private PendingIntent createCallAction(String sessionId, final String action, int requestCode) {
1701 return pendingServiceIntent(
1702 mXmppConnectionService,
1703 action,
1704 requestCode,
1705 ImmutableMap.of(RtpSessionActivity.EXTRA_SESSION_ID, sessionId));
1706 }
1707
1708 private PendingIntent createSnoozeIntent(final Conversation conversation) {
1709 return pendingServiceIntent(
1710 mXmppConnectionService,
1711 XmppConnectionService.ACTION_SNOOZE,
1712 generateRequestCode(conversation, 22),
1713 ImmutableMap.of("uuid", conversation.getUuid()));
1714 }
1715
1716 private static PendingIntent pendingServiceIntent(
1717 final Context context, final String action, final int requestCode) {
1718 return pendingServiceIntent(context, action, requestCode, ImmutableMap.of());
1719 }
1720
1721 private static PendingIntent pendingServiceIntent(
1722 final Context context,
1723 final String action,
1724 final int requestCode,
1725 final Map<String, String> extras) {
1726 final Intent intent = new Intent(context, XmppConnectionService.class);
1727 intent.setAction(action);
1728 for (final Map.Entry<String, String> entry : extras.entrySet()) {
1729 intent.putExtra(entry.getKey(), entry.getValue());
1730 }
1731 return PendingIntent.getService(
1732 context,
1733 requestCode,
1734 intent,
1735 s()
1736 ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1737 : PendingIntent.FLAG_UPDATE_CURRENT);
1738 }
1739
1740 private boolean wasHighlightedOrPrivate(final Message message) {
1741 if (message.getConversation() instanceof Conversation conversation) {
1742 final String nick = conversation.getMucOptions().getActualNick();
1743 final Pattern highlight = generateNickHighlightPattern(nick);
1744 if (message.getBody() == null || nick == null) {
1745 return false;
1746 }
1747 final Matcher m = highlight.matcher(message.getBody());
1748 return (m.find() || message.isPrivateMessage());
1749 } else {
1750 return false;
1751 }
1752 }
1753
1754 public void setOpenConversation(final Conversation conversation) {
1755 this.mOpenConversation = conversation;
1756 }
1757
1758 public void setIsInForeground(final boolean foreground) {
1759 this.mIsInForeground = foreground;
1760 }
1761
1762 private int getPixel(final int dp) {
1763 final DisplayMetrics metrics = mXmppConnectionService.getResources().getDisplayMetrics();
1764 return ((int) (dp * metrics.density));
1765 }
1766
1767 private void markLastNotification() {
1768 this.mLastNotification = SystemClock.elapsedRealtime();
1769 }
1770
1771 private boolean inMiniGracePeriod(final Account account) {
1772 final int miniGrace =
1773 account.getStatus() == Account.State.ONLINE
1774 ? Config.MINI_GRACE_PERIOD
1775 : Config.MINI_GRACE_PERIOD * 2;
1776 return SystemClock.elapsedRealtime() < (this.mLastNotification + miniGrace);
1777 }
1778
1779 Notification createForegroundNotification() {
1780 final Notification.Builder mBuilder = new Notification.Builder(mXmppConnectionService);
1781 mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.app_name));
1782 final List<Account> accounts = mXmppConnectionService.getAccounts();
1783 final int enabled;
1784 final int connected;
1785 if (accounts == null) {
1786 enabled = 0;
1787 connected = 0;
1788 } else {
1789 enabled = Iterables.size(Iterables.filter(accounts, Account::isEnabled));
1790 connected = Iterables.size(Iterables.filter(accounts, Account::isOnlineAndConnected));
1791 }
1792 mBuilder.setContentText(
1793 mXmppConnectionService.getString(R.string.connected_accounts, connected, enabled));
1794 final PendingIntent openIntent = createOpenConversationsIntent();
1795 if (openIntent != null) {
1796 mBuilder.setContentIntent(openIntent);
1797 }
1798 mBuilder.setWhen(0)
1799 .setPriority(Notification.PRIORITY_MIN)
1800 .setSmallIcon(connected > 0 ? R.drawable.ic_link_24dp : R.drawable.ic_link_off_24dp)
1801 .setLocalOnly(true);
1802
1803 if (Compatibility.runsTwentySix()) {
1804 mBuilder.setChannelId("foreground");
1805 mBuilder.addAction(
1806 R.drawable.ic_logout_24dp,
1807 mXmppConnectionService.getString(R.string.log_out),
1808 pendingServiceIntent(
1809 mXmppConnectionService,
1810 XmppConnectionService.ACTION_TEMPORARILY_DISABLE,
1811 87));
1812 mBuilder.addAction(
1813 R.drawable.ic_notifications_off_24dp,
1814 mXmppConnectionService.getString(R.string.hide_notification),
1815 pendingNotificationSettingsIntent(mXmppConnectionService));
1816 }
1817
1818 return mBuilder.build();
1819 }
1820
1821 @RequiresApi(api = Build.VERSION_CODES.O)
1822 private static PendingIntent pendingNotificationSettingsIntent(final Context context) {
1823 final Intent intent = new Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS);
1824 intent.putExtra(Settings.EXTRA_APP_PACKAGE, context.getPackageName());
1825 intent.putExtra(Settings.EXTRA_CHANNEL_ID, "foreground");
1826 return PendingIntent.getActivity(
1827 context,
1828 89,
1829 intent,
1830 s()
1831 ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1832 : PendingIntent.FLAG_UPDATE_CURRENT);
1833 }
1834
1835 private PendingIntent createOpenConversationsIntent() {
1836 try {
1837 return PendingIntent.getActivity(
1838 mXmppConnectionService,
1839 0,
1840 new Intent(mXmppConnectionService, ConversationsActivity.class),
1841 s()
1842 ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1843 : PendingIntent.FLAG_UPDATE_CURRENT);
1844 } catch (RuntimeException e) {
1845 return null;
1846 }
1847 }
1848
1849 void updateErrorNotification() {
1850 if (Config.SUPPRESS_ERROR_NOTIFICATION) {
1851 cancel(ERROR_NOTIFICATION_ID);
1852 return;
1853 }
1854 final boolean showAllErrors = QuickConversationsService.isConversations();
1855 final List<Account> errors = new ArrayList<>();
1856 boolean torNotAvailable = false;
1857 for (final Account account : mXmppConnectionService.getAccounts()) {
1858 if (account.hasErrorStatus()
1859 && account.showErrorNotification()
1860 && (showAllErrors
1861 || account.getLastErrorStatus() == Account.State.UNAUTHORIZED)) {
1862 errors.add(account);
1863 torNotAvailable |= account.getStatus() == Account.State.TOR_NOT_AVAILABLE;
1864 }
1865 }
1866 if (mXmppConnectionService.foregroundNotificationNeedsUpdatingWhenErrorStateChanges()) {
1867 try {
1868 notify(FOREGROUND_NOTIFICATION_ID, createForegroundNotification());
1869 } catch (final RuntimeException e) {
1870 Log.d(
1871 Config.LOGTAG,
1872 "not refreshing foreground service notification because service has died",
1873 e);
1874 }
1875 }
1876 final Notification.Builder mBuilder = new Notification.Builder(mXmppConnectionService);
1877 if (errors.isEmpty()) {
1878 cancel(ERROR_NOTIFICATION_ID);
1879 return;
1880 } else if (errors.size() == 1) {
1881 mBuilder.setContentTitle(
1882 mXmppConnectionService.getString(R.string.problem_connecting_to_account));
1883 mBuilder.setContentText(errors.get(0).getJid().asBareJid().toEscapedString());
1884 } else {
1885 mBuilder.setContentTitle(
1886 mXmppConnectionService.getString(R.string.problem_connecting_to_accounts));
1887 mBuilder.setContentText(mXmppConnectionService.getString(R.string.touch_to_fix));
1888 }
1889 try {
1890 mBuilder.addAction(
1891 R.drawable.ic_autorenew_24dp,
1892 mXmppConnectionService.getString(R.string.try_again),
1893 pendingServiceIntent(
1894 mXmppConnectionService, XmppConnectionService.ACTION_TRY_AGAIN, 45));
1895 mBuilder.setDeleteIntent(
1896 pendingServiceIntent(
1897 mXmppConnectionService,
1898 XmppConnectionService.ACTION_DISMISS_ERROR_NOTIFICATIONS,
1899 69));
1900 } catch (final RuntimeException e) {
1901 Log.d(
1902 Config.LOGTAG,
1903 "not including some actions in error notification because service has died",
1904 e);
1905 }
1906 if (torNotAvailable) {
1907 if (TorServiceUtils.isOrbotInstalled(mXmppConnectionService)) {
1908 mBuilder.addAction(
1909 R.drawable.ic_play_circle_24dp,
1910 mXmppConnectionService.getString(R.string.start_orbot),
1911 PendingIntent.getActivity(
1912 mXmppConnectionService,
1913 147,
1914 TorServiceUtils.LAUNCH_INTENT,
1915 s()
1916 ? PendingIntent.FLAG_IMMUTABLE
1917 | PendingIntent.FLAG_UPDATE_CURRENT
1918 : PendingIntent.FLAG_UPDATE_CURRENT));
1919 } else {
1920 mBuilder.addAction(
1921 R.drawable.ic_download_24dp,
1922 mXmppConnectionService.getString(R.string.install_orbot),
1923 PendingIntent.getActivity(
1924 mXmppConnectionService,
1925 146,
1926 TorServiceUtils.INSTALL_INTENT,
1927 s()
1928 ? PendingIntent.FLAG_IMMUTABLE
1929 | PendingIntent.FLAG_UPDATE_CURRENT
1930 : PendingIntent.FLAG_UPDATE_CURRENT));
1931 }
1932 }
1933 mBuilder.setVisibility(Notification.VISIBILITY_PRIVATE);
1934 mBuilder.setSmallIcon(R.drawable.ic_warning_24dp);
1935 mBuilder.setLocalOnly(true);
1936 mBuilder.setPriority(Notification.PRIORITY_LOW);
1937 final Intent intent;
1938 if (AccountUtils.MANAGE_ACCOUNT_ACTIVITY != null) {
1939 intent = new Intent(mXmppConnectionService, AccountUtils.MANAGE_ACCOUNT_ACTIVITY);
1940 } else {
1941 intent = new Intent(mXmppConnectionService, EditAccountActivity.class);
1942 intent.putExtra("jid", errors.get(0).getJid().asBareJid().toEscapedString());
1943 intent.putExtra(EditAccountActivity.EXTRA_OPENED_FROM_NOTIFICATION, true);
1944 }
1945 mBuilder.setContentIntent(
1946 PendingIntent.getActivity(
1947 mXmppConnectionService,
1948 145,
1949 intent,
1950 s()
1951 ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1952 : PendingIntent.FLAG_UPDATE_CURRENT));
1953 if (Compatibility.runsTwentySix()) {
1954 mBuilder.setChannelId("error");
1955 }
1956 notify(ERROR_NOTIFICATION_ID, mBuilder.build());
1957 }
1958
1959 void updateFileAddingNotification(final int current, final Message message) {
1960
1961 final Notification notification = videoTranscoding(current, message);
1962 notify(ONGOING_VIDEO_TRANSCODING_NOTIFICATION_ID, notification);
1963 }
1964
1965 private Notification videoTranscoding(final int current, @Nullable final Message message) {
1966 final Notification.Builder builder = new Notification.Builder(mXmppConnectionService);
1967 builder.setContentTitle(mXmppConnectionService.getString(R.string.transcoding_video));
1968 if (current >= 0) {
1969 builder.setProgress(100, current, false);
1970 } else {
1971 builder.setProgress(100, 0, true);
1972 }
1973 builder.setSmallIcon(R.drawable.ic_hourglass_top_24dp);
1974 if (message != null) {
1975 builder.setContentIntent(createContentIntent(message.getConversation()));
1976 }
1977 builder.setOngoing(true);
1978 if (Compatibility.runsTwentySix()) {
1979 builder.setChannelId("compression");
1980 }
1981 return builder.build();
1982 }
1983
1984 public Notification getIndeterminateVideoTranscoding() {
1985 return videoTranscoding(-1, null);
1986 }
1987
1988 private void notify(final String tag, final int id, final Notification notification) {
1989 if (ActivityCompat.checkSelfPermission(
1990 mXmppConnectionService, Manifest.permission.POST_NOTIFICATIONS)
1991 != PackageManager.PERMISSION_GRANTED) {
1992 return;
1993 }
1994 final var notificationManager =
1995 mXmppConnectionService.getSystemService(NotificationManager.class);
1996 try {
1997 notificationManager.notify(tag, id, notification);
1998 } catch (final RuntimeException e) {
1999 Log.d(Config.LOGTAG, "unable to make notification", e);
2000 }
2001 }
2002
2003 public void notify(final int id, final Notification notification) {
2004 if (ActivityCompat.checkSelfPermission(
2005 mXmppConnectionService, Manifest.permission.POST_NOTIFICATIONS)
2006 != PackageManager.PERMISSION_GRANTED) {
2007 return;
2008 }
2009 final var notificationManager =
2010 mXmppConnectionService.getSystemService(NotificationManager.class);
2011 try {
2012 notificationManager.notify(id, notification);
2013 } catch (final RuntimeException e) {
2014 Log.d(Config.LOGTAG, "unable to make notification", e);
2015 }
2016 }
2017
2018 public void cancel(final int id) {
2019 final NotificationManagerCompat notificationManager =
2020 NotificationManagerCompat.from(mXmppConnectionService);
2021 try {
2022 notificationManager.cancel(id);
2023 } catch (RuntimeException e) {
2024 Log.d(Config.LOGTAG, "unable to cancel notification", e);
2025 }
2026 }
2027
2028 private void cancel(String tag, int id) {
2029 final NotificationManagerCompat notificationManager =
2030 NotificationManagerCompat.from(mXmppConnectionService);
2031 try {
2032 notificationManager.cancel(tag, id);
2033 } catch (RuntimeException e) {
2034 Log.d(Config.LOGTAG, "unable to cancel notification", e);
2035 }
2036 }
2037
2038 private static class MissedCallsInfo {
2039 private int numberOfCalls;
2040 private long lastTime;
2041
2042 MissedCallsInfo(final long time) {
2043 numberOfCalls = 1;
2044 lastTime = time;
2045 }
2046
2047 public void newMissedCall(final long time) {
2048 ++numberOfCalls;
2049 lastTime = time;
2050 }
2051
2052 public boolean removeMissedCall() {
2053 --numberOfCalls;
2054 return numberOfCalls <= 0;
2055 }
2056
2057 public int getNumberOfCalls() {
2058 return numberOfCalls;
2059 }
2060
2061 public long getLastTime() {
2062 return lastTime;
2063 }
2064 }
2065}