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