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