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