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