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