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 =
506 (NotificationManager)
507 mXmppConnectionService.getSystemService(Context.NOTIFICATION_SERVICE);
508 final int currentInterruptionFilter;
509 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && notificationManager != null) {
510 currentInterruptionFilter = notificationManager.getCurrentInterruptionFilter();
511 } else {
512 currentInterruptionFilter = 1; // INTERRUPTION_FILTER_ALL
513 }
514 if (currentInterruptionFilter != 1) {
515 Log.d(
516 Config.LOGTAG,
517 "do not ring or vibrate because interruption filter has been set to "
518 + currentInterruptionFilter);
519 return;
520 }
521 final ScheduledFuture<?> currentVibrationFuture = this.vibrationFuture;
522 this.vibrationFuture =
523 SCHEDULED_EXECUTOR_SERVICE.scheduleAtFixedRate(
524 new VibrationRunnable(), 0, 3, TimeUnit.SECONDS);
525 if (currentVibrationFuture != null) {
526 currentVibrationFuture.cancel(true);
527 }
528 final SharedPreferences preferences =
529 PreferenceManager.getDefaultSharedPreferences(mXmppConnectionService);
530 final Resources resources = mXmppConnectionService.getResources();
531 final String ringtonePreference =
532 preferences.getString(
533 "call_ringtone", resources.getString(R.string.incoming_call_ringtone));
534 if (Strings.isNullOrEmpty(ringtonePreference)) {
535 Log.d(Config.LOGTAG, "ringtone has been set to none");
536 return;
537 }
538 final Uri uri = Uri.parse(ringtonePreference);
539 this.currentlyPlayingRingtone = RingtoneManager.getRingtone(mXmppConnectionService, uri);
540 if (this.currentlyPlayingRingtone == null) {
541 Log.d(Config.LOGTAG, "unable to find ringtone for uri " + uri);
542 return;
543 }
544 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
545 this.currentlyPlayingRingtone.setLooping(true);
546 }
547 this.currentlyPlayingRingtone.play();
548 }
549
550 private void showIncomingCallNotification(
551 final AbstractJingleConnection.Id id, final Set<Media> media) {
552 final Intent fullScreenIntent =
553 new Intent(mXmppConnectionService, RtpSessionActivity.class);
554 fullScreenIntent.putExtra(
555 RtpSessionActivity.EXTRA_ACCOUNT,
556 id.account.getJid().asBareJid().toEscapedString());
557 fullScreenIntent.putExtra(RtpSessionActivity.EXTRA_WITH, id.with.toEscapedString());
558 fullScreenIntent.putExtra(RtpSessionActivity.EXTRA_SESSION_ID, id.sessionId);
559 fullScreenIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
560 fullScreenIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
561 final NotificationCompat.Builder builder =
562 new NotificationCompat.Builder(
563 mXmppConnectionService, INCOMING_CALLS_NOTIFICATION_CHANNEL);
564 if (media.contains(Media.VIDEO)) {
565 builder.setSmallIcon(R.drawable.ic_videocam_white_24dp);
566 builder.setContentTitle(
567 mXmppConnectionService.getString(R.string.rtp_state_incoming_video_call));
568 } else {
569 builder.setSmallIcon(R.drawable.ic_call_white_24dp);
570 builder.setContentTitle(
571 mXmppConnectionService.getString(R.string.rtp_state_incoming_call));
572 }
573 final Contact contact = id.getContact();
574 builder.setLargeIcon(
575 mXmppConnectionService
576 .getAvatarService()
577 .get(contact, AvatarService.getSystemUiAvatarSize(mXmppConnectionService)));
578 final Uri systemAccount = contact.getSystemAccount();
579 if (systemAccount != null) {
580 builder.addPerson(systemAccount.toString());
581 }
582 builder.setContentText(id.account.getRoster().getContact(id.with).getDisplayName());
583 builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC);
584 builder.setPriority(NotificationCompat.PRIORITY_HIGH);
585 builder.setCategory(NotificationCompat.CATEGORY_CALL);
586 PendingIntent pendingIntent = createPendingRtpSession(id, Intent.ACTION_VIEW, 101);
587 builder.setFullScreenIntent(pendingIntent, true);
588 builder.setContentIntent(pendingIntent); // old androids need this?
589 builder.setOngoing(true);
590 builder.addAction(
591 new NotificationCompat.Action.Builder(
592 R.drawable.ic_call_end_white_48dp,
593 mXmppConnectionService.getString(R.string.dismiss_call),
594 createCallAction(
595 id.sessionId,
596 XmppConnectionService.ACTION_DISMISS_CALL,
597 102))
598 .build());
599 builder.addAction(
600 new NotificationCompat.Action.Builder(
601 R.drawable.ic_call_white_24dp,
602 mXmppConnectionService.getString(R.string.answer_call),
603 createPendingRtpSession(
604 id, RtpSessionActivity.ACTION_ACCEPT_CALL, 103))
605 .build());
606 modifyIncomingCall(builder);
607 final Notification notification = builder.build();
608 notification.flags = notification.flags | Notification.FLAG_INSISTENT;
609 notify(INCOMING_CALL_NOTIFICATION_ID, notification);
610 }
611
612 public Notification getOngoingCallNotification(
613 final XmppConnectionService.OngoingCall ongoingCall) {
614 final AbstractJingleConnection.Id id = ongoingCall.id;
615 final NotificationCompat.Builder builder =
616 new NotificationCompat.Builder(mXmppConnectionService, "ongoing_calls");
617 if (ongoingCall.media.contains(Media.VIDEO)) {
618 builder.setSmallIcon(R.drawable.ic_videocam_white_24dp);
619 if (ongoingCall.reconnecting) {
620 builder.setContentTitle(
621 mXmppConnectionService.getString(R.string.reconnecting_video_call));
622 } else {
623 builder.setContentTitle(
624 mXmppConnectionService.getString(R.string.ongoing_video_call));
625 }
626 } else {
627 builder.setSmallIcon(R.drawable.ic_call_white_24dp);
628 if (ongoingCall.reconnecting) {
629 builder.setContentTitle(
630 mXmppConnectionService.getString(R.string.reconnecting_call));
631 } else {
632 builder.setContentTitle(mXmppConnectionService.getString(R.string.ongoing_call));
633 }
634 }
635 builder.setContentText(id.account.getRoster().getContact(id.with).getDisplayName());
636 builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC);
637 builder.setPriority(NotificationCompat.PRIORITY_HIGH);
638 builder.setCategory(NotificationCompat.CATEGORY_CALL);
639 builder.setContentIntent(createPendingRtpSession(id, Intent.ACTION_VIEW, 101));
640 builder.setOngoing(true);
641 builder.addAction(
642 new NotificationCompat.Action.Builder(
643 R.drawable.ic_call_end_white_48dp,
644 mXmppConnectionService.getString(R.string.hang_up),
645 createCallAction(
646 id.sessionId, XmppConnectionService.ACTION_END_CALL, 104))
647 .build());
648 builder.setLocalOnly(true);
649 return builder.build();
650 }
651
652 private PendingIntent createPendingRtpSession(
653 final AbstractJingleConnection.Id id, final String action, final int requestCode) {
654 final Intent fullScreenIntent =
655 new Intent(mXmppConnectionService, RtpSessionActivity.class);
656 fullScreenIntent.setAction(action);
657 fullScreenIntent.putExtra(
658 RtpSessionActivity.EXTRA_ACCOUNT,
659 id.account.getJid().asBareJid().toEscapedString());
660 fullScreenIntent.putExtra(RtpSessionActivity.EXTRA_WITH, id.with.toEscapedString());
661 fullScreenIntent.putExtra(RtpSessionActivity.EXTRA_SESSION_ID, id.sessionId);
662 return PendingIntent.getActivity(
663 mXmppConnectionService,
664 requestCode,
665 fullScreenIntent,
666 s()
667 ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
668 : PendingIntent.FLAG_UPDATE_CURRENT);
669 }
670
671 public void cancelIncomingCallNotification() {
672 stopSoundAndVibration();
673 cancel(INCOMING_CALL_NOTIFICATION_ID);
674 }
675
676 public boolean stopSoundAndVibration() {
677 int stopped = 0;
678 if (this.currentlyPlayingRingtone != null) {
679 if (this.currentlyPlayingRingtone.isPlaying()) {
680 Log.d(Config.LOGTAG, "stop playing ring tone");
681 ++stopped;
682 }
683 this.currentlyPlayingRingtone.stop();
684 }
685 if (this.vibrationFuture != null && !this.vibrationFuture.isCancelled()) {
686 Log.d(Config.LOGTAG, "stop vibration");
687 this.vibrationFuture.cancel(true);
688 ++stopped;
689 }
690 return stopped > 0;
691 }
692
693 public static void cancelIncomingCallNotification(final Context context) {
694 final NotificationManagerCompat notificationManager =
695 NotificationManagerCompat.from(context);
696 try {
697 notificationManager.cancel(INCOMING_CALL_NOTIFICATION_ID);
698 } catch (RuntimeException e) {
699 Log.d(Config.LOGTAG, "unable to cancel incoming call notification after crash", e);
700 }
701 }
702
703 private void pushNow(final Message message) {
704 mXmppConnectionService.updateUnreadCountBadge();
705 if (!notifyMessage(message)) {
706 Log.d(
707 Config.LOGTAG,
708 message.getConversation().getAccount().getJid().asBareJid()
709 + ": suppressing notification because turned off");
710 return;
711 }
712 final boolean isScreenLocked = mXmppConnectionService.isScreenLocked();
713 if (this.mIsInForeground
714 && !isScreenLocked
715 && this.mOpenConversation == message.getConversation()) {
716 Log.d(
717 Config.LOGTAG,
718 message.getConversation().getAccount().getJid().asBareJid()
719 + ": suppressing notification because conversation is open");
720 return;
721 }
722 synchronized (notifications) {
723 pushToStack(message);
724 final Conversational conversation = message.getConversation();
725 final Account account = conversation.getAccount();
726 final boolean doNotify =
727 (!(this.mIsInForeground && this.mOpenConversation == null) || isScreenLocked)
728 && !account.inGracePeriod()
729 && !this.inMiniGracePeriod(account);
730 updateNotification(doNotify, Collections.singletonList(conversation.getUuid()));
731 }
732 }
733
734 private void pushMissedCall(final Message message) {
735 final Conversational conversation = message.getConversation();
736 final MissedCallsInfo info = mMissedCalls.get(conversation);
737 if (info == null) {
738 mMissedCalls.put(conversation, new MissedCallsInfo(message.getTimeSent()));
739 } else {
740 info.newMissedCall(message.getTimeSent());
741 }
742 }
743
744 public void pushMissedCallNow(final Message message) {
745 synchronized (mMissedCalls) {
746 pushMissedCall(message);
747 updateMissedCallNotifications(Collections.singleton(message.getConversation()));
748 }
749 }
750
751 public void clear(final Conversation conversation) {
752 clearMessages(conversation);
753 clearMissedCalls(conversation);
754 }
755
756 public void clearMessages() {
757 synchronized (notifications) {
758 for (ArrayList<Message> messages : notifications.values()) {
759 markAsReadIfHasDirectReply(messages);
760 }
761 notifications.clear();
762 updateNotification(false);
763 }
764 }
765
766 public void clearMessages(final Conversation conversation) {
767 synchronized (this.mBacklogMessageCounter) {
768 this.mBacklogMessageCounter.remove(conversation);
769 }
770 synchronized (notifications) {
771 markAsReadIfHasDirectReply(conversation);
772 if (notifications.remove(conversation.getUuid()) != null) {
773 cancel(conversation.getUuid(), NOTIFICATION_ID);
774 updateNotification(false, null, true);
775 }
776 }
777 }
778
779 public void clearMissedCall(final Message message) {
780 synchronized (mMissedCalls) {
781 final Iterator<Map.Entry<Conversational,MissedCallsInfo>> iterator = mMissedCalls.entrySet().iterator();
782 while (iterator.hasNext()) {
783 final Map.Entry<Conversational, MissedCallsInfo> entry = iterator.next();
784 final Conversational conversational = entry.getKey();
785 final MissedCallsInfo missedCallsInfo = entry.getValue();
786 if (conversational.getUuid().equals(message.getConversation().getUuid())) {
787 if (missedCallsInfo.removeMissedCall()) {
788 cancel(conversational.getUuid(), MISSED_CALL_NOTIFICATION_ID);
789 Log.d(Config.LOGTAG,conversational.getAccount().getJid().asBareJid()+": dismissed missed call because call was picked up on other device");
790 iterator.remove();
791 }
792 }
793 }
794 updateMissedCallNotifications(null);
795 }
796 }
797
798 public void clearMissedCalls() {
799 synchronized (mMissedCalls) {
800 for (final Conversational conversation : mMissedCalls.keySet()) {
801 cancel(conversation.getUuid(), MISSED_CALL_NOTIFICATION_ID);
802 }
803 mMissedCalls.clear();
804 updateMissedCallNotifications(null);
805 }
806 }
807
808 public void clearMissedCalls(final Conversation conversation) {
809 synchronized (mMissedCalls) {
810 if (mMissedCalls.remove(conversation) != null) {
811 cancel(conversation.getUuid(), MISSED_CALL_NOTIFICATION_ID);
812 updateMissedCallNotifications(null);
813 }
814 }
815 }
816
817 private void markAsReadIfHasDirectReply(final Conversation conversation) {
818 markAsReadIfHasDirectReply(notifications.get(conversation.getUuid()));
819 }
820
821 private void markAsReadIfHasDirectReply(final ArrayList<Message> messages) {
822 if (messages != null && messages.size() > 0) {
823 Message last = messages.get(messages.size() - 1);
824 if (last.getStatus() != Message.STATUS_RECEIVED) {
825 if (mXmppConnectionService.markRead((Conversation) last.getConversation(), false)) {
826 mXmppConnectionService.updateConversationUi();
827 }
828 }
829 }
830 }
831
832 private void setNotificationColor(final Builder mBuilder) {
833 mBuilder.setColor(ContextCompat.getColor(mXmppConnectionService, R.color.green600));
834 }
835
836 public void updateNotification() {
837 synchronized (notifications) {
838 updateNotification(false);
839 }
840 }
841
842 private void updateNotification(final boolean notify) {
843 updateNotification(notify, null, false);
844 }
845
846 private void updateNotification(final boolean notify, final List<String> conversations) {
847 updateNotification(notify, conversations, false);
848 }
849
850 private void updateNotification(
851 final boolean notify, final List<String> conversations, final boolean summaryOnly) {
852 final SharedPreferences preferences =
853 PreferenceManager.getDefaultSharedPreferences(mXmppConnectionService);
854
855 final boolean quiteHours = isQuietHours();
856
857 final boolean notifyOnlyOneChild =
858 notify
859 && conversations != null
860 && conversations.size()
861 == 1; // if this check is changed to > 0 catchup messages will
862 // create one notification per conversation
863
864 if (notifications.size() == 0) {
865 cancel(NOTIFICATION_ID);
866 } else {
867 if (notify) {
868 this.markLastNotification();
869 }
870 final Builder mBuilder;
871 if (notifications.size() == 1 && Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
872 mBuilder =
873 buildSingleConversations(
874 notifications.values().iterator().next(), notify, quiteHours);
875 modifyForSoundVibrationAndLight(mBuilder, notify, quiteHours, preferences);
876 notify(NOTIFICATION_ID, mBuilder.build());
877 } else {
878 mBuilder = buildMultipleConversation(notify, quiteHours);
879 if (notifyOnlyOneChild) {
880 mBuilder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN);
881 }
882 modifyForSoundVibrationAndLight(mBuilder, notify, quiteHours, preferences);
883 if (!summaryOnly) {
884 for (Map.Entry<String, ArrayList<Message>> entry : notifications.entrySet()) {
885 String uuid = entry.getKey();
886 final boolean notifyThis =
887 notifyOnlyOneChild ? conversations.contains(uuid) : notify;
888 Builder singleBuilder =
889 buildSingleConversations(entry.getValue(), notifyThis, quiteHours);
890 if (!notifyOnlyOneChild) {
891 singleBuilder.setGroupAlertBehavior(
892 NotificationCompat.GROUP_ALERT_SUMMARY);
893 }
894 modifyForSoundVibrationAndLight(
895 singleBuilder, notifyThis, quiteHours, preferences);
896 singleBuilder.setGroup(MESSAGES_GROUP);
897 setNotificationColor(singleBuilder);
898 notify(entry.getKey(), NOTIFICATION_ID, singleBuilder.build());
899 }
900 }
901 notify(NOTIFICATION_ID, mBuilder.build());
902 }
903 }
904 }
905
906 private void updateMissedCallNotifications(final Set<Conversational> update) {
907 if (mMissedCalls.isEmpty()) {
908 cancel(MISSED_CALL_NOTIFICATION_ID);
909 return;
910 }
911 if (mMissedCalls.size() == 1 && Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
912 final Conversational conversation = mMissedCalls.keySet().iterator().next();
913 final MissedCallsInfo info = mMissedCalls.values().iterator().next();
914 final Notification notification = missedCall(conversation, info);
915 notify(MISSED_CALL_NOTIFICATION_ID, notification);
916 } else {
917 final Notification summary = missedCallsSummary();
918 notify(MISSED_CALL_NOTIFICATION_ID, summary);
919 if (update != null) {
920 for (final Conversational conversation : update) {
921 final MissedCallsInfo info = mMissedCalls.get(conversation);
922 if (info != null) {
923 final Notification notification = missedCall(conversation, info);
924 notify(conversation.getUuid(), MISSED_CALL_NOTIFICATION_ID, notification);
925 }
926 }
927 }
928 }
929 }
930
931 private void modifyForSoundVibrationAndLight(
932 Builder mBuilder, boolean notify, boolean quietHours, SharedPreferences preferences) {
933 final Resources resources = mXmppConnectionService.getResources();
934 final String ringtone =
935 preferences.getString(
936 "notification_ringtone",
937 resources.getString(R.string.notification_ringtone));
938 final boolean vibrate =
939 preferences.getBoolean(
940 "vibrate_on_notification",
941 resources.getBoolean(R.bool.vibrate_on_notification));
942 final boolean led = preferences.getBoolean("led", resources.getBoolean(R.bool.led));
943 final boolean headsup =
944 preferences.getBoolean(
945 "notification_headsup", resources.getBoolean(R.bool.headsup_notifications));
946 if (notify && !quietHours) {
947 if (vibrate) {
948 final int dat = 70;
949 final long[] pattern = {0, 3 * dat, dat, dat};
950 mBuilder.setVibrate(pattern);
951 } else {
952 mBuilder.setVibrate(new long[] {0});
953 }
954 Uri uri = Uri.parse(ringtone);
955 try {
956 mBuilder.setSound(fixRingtoneUri(uri));
957 } catch (SecurityException e) {
958 Log.d(Config.LOGTAG, "unable to use custom notification sound " + uri.toString());
959 }
960 } else {
961 mBuilder.setLocalOnly(true);
962 }
963 mBuilder.setCategory(Notification.CATEGORY_MESSAGE);
964 mBuilder.setPriority(
965 notify
966 ? (headsup
967 ? NotificationCompat.PRIORITY_HIGH
968 : NotificationCompat.PRIORITY_DEFAULT)
969 : NotificationCompat.PRIORITY_LOW);
970 setNotificationColor(mBuilder);
971 mBuilder.setDefaults(0);
972 if (led) {
973 mBuilder.setLights(LED_COLOR, 2000, 3000);
974 }
975 }
976
977 private void modifyIncomingCall(final Builder mBuilder) {
978 mBuilder.setPriority(NotificationCompat.PRIORITY_HIGH);
979 setNotificationColor(mBuilder);
980 mBuilder.setLights(LED_COLOR, 2000, 3000);
981 }
982
983 private Uri fixRingtoneUri(Uri uri) {
984 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && "file".equals(uri.getScheme())) {
985 return FileBackend.getUriForFile(mXmppConnectionService, new File(uri.getPath()));
986 } else {
987 return uri;
988 }
989 }
990
991 private Notification missedCallsSummary() {
992 final Builder publicBuilder = buildMissedCallsSummary(true);
993 final Builder builder = buildMissedCallsSummary(false);
994 builder.setPublicVersion(publicBuilder.build());
995 return builder.build();
996 }
997
998 private Builder buildMissedCallsSummary(boolean publicVersion) {
999 final Builder builder =
1000 new NotificationCompat.Builder(mXmppConnectionService, "missed_calls");
1001 int totalCalls = 0;
1002 final List<String> names = new ArrayList<>();
1003 long lastTime = 0;
1004 for (final Map.Entry<Conversational, MissedCallsInfo> entry : mMissedCalls.entrySet()) {
1005 final Conversational conversation = entry.getKey();
1006 final MissedCallsInfo missedCallsInfo = entry.getValue();
1007 names.add(conversation.getContact().getDisplayName());
1008 totalCalls += missedCallsInfo.getNumberOfCalls();
1009 lastTime = Math.max(lastTime, missedCallsInfo.getLastTime());
1010 }
1011 final String title =
1012 (totalCalls == 1)
1013 ? mXmppConnectionService.getString(R.string.missed_call)
1014 : (mMissedCalls.size() == 1)
1015 ? mXmppConnectionService
1016 .getResources()
1017 .getQuantityString(
1018 R.plurals.n_missed_calls, totalCalls, totalCalls)
1019 : mXmppConnectionService
1020 .getResources()
1021 .getQuantityString(
1022 R.plurals.n_missed_calls_from_m_contacts,
1023 mMissedCalls.size(),
1024 totalCalls,
1025 mMissedCalls.size());
1026 builder.setContentTitle(title);
1027 builder.setTicker(title);
1028 if (!publicVersion) {
1029 builder.setContentText(Joiner.on(", ").join(names));
1030 }
1031 builder.setSmallIcon(R.drawable.ic_call_missed_white_24db);
1032 builder.setGroupSummary(true);
1033 builder.setGroup(MISSED_CALLS_GROUP);
1034 builder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN);
1035 builder.setCategory(NotificationCompat.CATEGORY_CALL);
1036 builder.setWhen(lastTime);
1037 if (!mMissedCalls.isEmpty()) {
1038 final Conversational firstConversation = mMissedCalls.keySet().iterator().next();
1039 builder.setContentIntent(createContentIntent(firstConversation));
1040 }
1041 builder.setDeleteIntent(createMissedCallsDeleteIntent(null));
1042 modifyMissedCall(builder);
1043 return builder;
1044 }
1045
1046 private Notification missedCall(final Conversational conversation, final MissedCallsInfo info) {
1047 final Builder publicBuilder = buildMissedCall(conversation, info, true);
1048 final Builder builder = buildMissedCall(conversation, info, false);
1049 builder.setPublicVersion(publicBuilder.build());
1050 return builder.build();
1051 }
1052
1053 private Builder buildMissedCall(
1054 final Conversational conversation, final MissedCallsInfo info, boolean publicVersion) {
1055 final Builder builder =
1056 new NotificationCompat.Builder(mXmppConnectionService, "missed_calls");
1057 final String title =
1058 (info.getNumberOfCalls() == 1)
1059 ? mXmppConnectionService.getString(R.string.missed_call)
1060 : mXmppConnectionService
1061 .getResources()
1062 .getQuantityString(
1063 R.plurals.n_missed_calls,
1064 info.getNumberOfCalls(),
1065 info.getNumberOfCalls());
1066 builder.setContentTitle(title);
1067 final String name = conversation.getContact().getDisplayName();
1068 if (publicVersion) {
1069 builder.setTicker(title);
1070 } else {
1071 builder.setTicker(
1072 mXmppConnectionService
1073 .getResources()
1074 .getQuantityString(
1075 R.plurals.n_missed_calls_from_x,
1076 info.getNumberOfCalls(),
1077 info.getNumberOfCalls(),
1078 name));
1079 builder.setContentText(name);
1080 }
1081 builder.setSmallIcon(R.drawable.ic_call_missed_white_24db);
1082 builder.setGroup(MISSED_CALLS_GROUP);
1083 builder.setCategory(NotificationCompat.CATEGORY_CALL);
1084 builder.setWhen(info.getLastTime());
1085 builder.setContentIntent(createContentIntent(conversation));
1086 builder.setDeleteIntent(createMissedCallsDeleteIntent(conversation));
1087 if (!publicVersion && conversation instanceof Conversation) {
1088 builder.setLargeIcon(
1089 mXmppConnectionService
1090 .getAvatarService()
1091 .get(
1092 (Conversation) conversation,
1093 AvatarService.getSystemUiAvatarSize(mXmppConnectionService)));
1094 }
1095 modifyMissedCall(builder);
1096 return builder;
1097 }
1098
1099 private void modifyMissedCall(final Builder builder) {
1100 final SharedPreferences preferences =
1101 PreferenceManager.getDefaultSharedPreferences(mXmppConnectionService);
1102 final Resources resources = mXmppConnectionService.getResources();
1103 final boolean led = preferences.getBoolean("led", resources.getBoolean(R.bool.led));
1104 if (led) {
1105 builder.setLights(LED_COLOR, 2000, 3000);
1106 }
1107 builder.setPriority(NotificationCompat.PRIORITY_HIGH);
1108 builder.setSound(null);
1109 setNotificationColor(builder);
1110 }
1111
1112 private Builder buildMultipleConversation(final boolean notify, final boolean quietHours) {
1113 final Builder mBuilder =
1114 new NotificationCompat.Builder(
1115 mXmppConnectionService,
1116 quietHours ? "quiet_hours" : (notify ? MESSAGES_NOTIFICATION_CHANNEL : "silent_messages"));
1117 final NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle();
1118 style.setBigContentTitle(
1119 mXmppConnectionService
1120 .getResources()
1121 .getQuantityString(
1122 R.plurals.x_unread_conversations,
1123 notifications.size(),
1124 notifications.size()));
1125 final List<String> names = new ArrayList<>();
1126 Conversation conversation = null;
1127 for (final ArrayList<Message> messages : notifications.values()) {
1128 if (messages.isEmpty()) {
1129 continue;
1130 }
1131 conversation = (Conversation) messages.get(0).getConversation();
1132 final String name = conversation.getName().toString();
1133 SpannableString styledString;
1134 if (Config.HIDE_MESSAGE_TEXT_IN_NOTIFICATION) {
1135 int count = messages.size();
1136 styledString =
1137 new SpannableString(
1138 name
1139 + ": "
1140 + mXmppConnectionService
1141 .getResources()
1142 .getQuantityString(
1143 R.plurals.x_messages, count, count));
1144 styledString.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), 0);
1145 style.addLine(styledString);
1146 } else {
1147 styledString =
1148 new SpannableString(
1149 name
1150 + ": "
1151 + UIHelper.getMessagePreview(
1152 mXmppConnectionService, messages.get(0))
1153 .first);
1154 styledString.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), 0);
1155 style.addLine(styledString);
1156 }
1157 names.add(name);
1158 }
1159 final String contentTitle =
1160 mXmppConnectionService
1161 .getResources()
1162 .getQuantityString(
1163 R.plurals.x_unread_conversations,
1164 notifications.size(),
1165 notifications.size());
1166 mBuilder.setContentTitle(contentTitle);
1167 mBuilder.setTicker(contentTitle);
1168 mBuilder.setContentText(Joiner.on(", ").join(names));
1169 mBuilder.setStyle(style);
1170 if (conversation != null) {
1171 mBuilder.setContentIntent(createContentIntent(conversation));
1172 }
1173 mBuilder.setGroupSummary(true);
1174 mBuilder.setGroup(MESSAGES_GROUP);
1175 mBuilder.setDeleteIntent(createDeleteIntent(null));
1176 mBuilder.setSmallIcon(R.drawable.ic_notification);
1177 return mBuilder;
1178 }
1179
1180 private Builder buildSingleConversations(
1181 final ArrayList<Message> messages, final boolean notify, final boolean quietHours) {
1182 final var channel = quietHours ? "quiet_hours" : (notify ? MESSAGES_NOTIFICATION_CHANNEL : "silent_messages");
1183 final Builder notificationBuilder =
1184 new NotificationCompat.Builder(mXmppConnectionService, channel);
1185 if (messages.isEmpty()) {
1186 return notificationBuilder;
1187 }
1188 final Conversation conversation = (Conversation) messages.get(0).getConversation();
1189 notificationBuilder.setLargeIcon(
1190 mXmppConnectionService
1191 .getAvatarService()
1192 .get(
1193 conversation,
1194 AvatarService.getSystemUiAvatarSize(mXmppConnectionService)));
1195 notificationBuilder.setContentTitle(conversation.getName());
1196 if (Config.HIDE_MESSAGE_TEXT_IN_NOTIFICATION) {
1197 int count = messages.size();
1198 notificationBuilder.setContentText(
1199 mXmppConnectionService
1200 .getResources()
1201 .getQuantityString(R.plurals.x_messages, count, count));
1202 } else {
1203 final Message message;
1204 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P
1205 && (message = getImage(messages)) != null) {
1206 modifyForImage(notificationBuilder, message, messages);
1207 } else {
1208 modifyForTextOnly(notificationBuilder, messages);
1209 }
1210 RemoteInput remoteInput =
1211 new RemoteInput.Builder("text_reply")
1212 .setLabel(UIHelper.getMessageHint(mXmppConnectionService, conversation))
1213 .build();
1214 PendingIntent markAsReadPendingIntent = createReadPendingIntent(conversation);
1215 NotificationCompat.Action markReadAction =
1216 new NotificationCompat.Action.Builder(
1217 R.drawable.ic_drafts_white_24dp,
1218 mXmppConnectionService.getString(R.string.mark_as_read),
1219 markAsReadPendingIntent)
1220 .setSemanticAction(
1221 NotificationCompat.Action.SEMANTIC_ACTION_MARK_AS_READ)
1222 .setShowsUserInterface(false)
1223 .build();
1224 final String replyLabel = mXmppConnectionService.getString(R.string.reply);
1225 final String lastMessageUuid = Iterables.getLast(messages).getUuid();
1226 final NotificationCompat.Action replyAction =
1227 new NotificationCompat.Action.Builder(
1228 R.drawable.ic_send_text_offline,
1229 replyLabel,
1230 createReplyIntent(conversation, lastMessageUuid, false))
1231 .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY)
1232 .setShowsUserInterface(false)
1233 .addRemoteInput(remoteInput)
1234 .build();
1235 final NotificationCompat.Action wearReplyAction =
1236 new NotificationCompat.Action.Builder(
1237 R.drawable.ic_wear_reply,
1238 replyLabel,
1239 createReplyIntent(conversation, lastMessageUuid, true))
1240 .addRemoteInput(remoteInput)
1241 .build();
1242 notificationBuilder.extend(
1243 new NotificationCompat.WearableExtender().addAction(wearReplyAction));
1244 int addedActionsCount = 1;
1245 notificationBuilder.addAction(markReadAction);
1246 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
1247 notificationBuilder.addAction(replyAction);
1248 ++addedActionsCount;
1249 }
1250
1251 if (displaySnoozeAction(messages)) {
1252 String label = mXmppConnectionService.getString(R.string.snooze);
1253 PendingIntent pendingSnoozeIntent = createSnoozeIntent(conversation);
1254 NotificationCompat.Action snoozeAction =
1255 new NotificationCompat.Action.Builder(
1256 R.drawable.ic_notifications_paused_white_24dp,
1257 label,
1258 pendingSnoozeIntent)
1259 .build();
1260 notificationBuilder.addAction(snoozeAction);
1261 ++addedActionsCount;
1262 }
1263 if (addedActionsCount < 3) {
1264 final Message firstLocationMessage = getFirstLocationMessage(messages);
1265 if (firstLocationMessage != null) {
1266 final PendingIntent pendingShowLocationIntent =
1267 createShowLocationIntent(firstLocationMessage);
1268 if (pendingShowLocationIntent != null) {
1269 final String label =
1270 mXmppConnectionService
1271 .getResources()
1272 .getString(R.string.show_location);
1273 NotificationCompat.Action locationAction =
1274 new NotificationCompat.Action.Builder(
1275 R.drawable.ic_room_white_24dp,
1276 label,
1277 pendingShowLocationIntent)
1278 .build();
1279 notificationBuilder.addAction(locationAction);
1280 ++addedActionsCount;
1281 }
1282 }
1283 }
1284 if (addedActionsCount < 3) {
1285 Message firstDownloadableMessage = getFirstDownloadableMessage(messages);
1286 if (firstDownloadableMessage != null) {
1287 String label =
1288 mXmppConnectionService
1289 .getResources()
1290 .getString(
1291 R.string.download_x_file,
1292 UIHelper.getFileDescriptionString(
1293 mXmppConnectionService,
1294 firstDownloadableMessage));
1295 PendingIntent pendingDownloadIntent =
1296 createDownloadIntent(firstDownloadableMessage);
1297 NotificationCompat.Action downloadAction =
1298 new NotificationCompat.Action.Builder(
1299 R.drawable.ic_file_download_white_24dp,
1300 label,
1301 pendingDownloadIntent)
1302 .build();
1303 notificationBuilder.addAction(downloadAction);
1304 ++addedActionsCount;
1305 }
1306 }
1307 }
1308 final ShortcutInfoCompat info;
1309 if (conversation.getMode() == Conversation.MODE_SINGLE) {
1310 final Contact contact = conversation.getContact();
1311 final Uri systemAccount = contact.getSystemAccount();
1312 if (systemAccount != null) {
1313 notificationBuilder.addPerson(systemAccount.toString());
1314 }
1315 info = mXmppConnectionService.getShortcutService().getShortcutInfoCompat(contact);
1316 } else {
1317 info =
1318 mXmppConnectionService
1319 .getShortcutService()
1320 .getShortcutInfoCompat(conversation.getMucOptions());
1321 }
1322 notificationBuilder.setWhen(conversation.getLatestMessage().getTimeSent());
1323 notificationBuilder.setSmallIcon(R.drawable.ic_notification);
1324 notificationBuilder.setDeleteIntent(createDeleteIntent(conversation));
1325 notificationBuilder.setContentIntent(createContentIntent(conversation));
1326 if (channel.equals(MESSAGES_NOTIFICATION_CHANNEL)) {
1327 // when do not want 'customized' notifications for silent notifications in their
1328 // respective channels
1329 notificationBuilder.setShortcutInfo(info);
1330 if (Build.VERSION.SDK_INT >= 30) {
1331 mXmppConnectionService
1332 .getSystemService(ShortcutManager.class)
1333 .pushDynamicShortcut(info.toShortcutInfo());
1334 }
1335 }
1336 return notificationBuilder;
1337 }
1338
1339 private void modifyForImage(
1340 final Builder builder, final Message message, final ArrayList<Message> messages) {
1341 try {
1342 final Bitmap bitmap =
1343 mXmppConnectionService
1344 .getFileBackend()
1345 .getThumbnail(message, getPixel(288), false);
1346 final ArrayList<Message> tmp = new ArrayList<>();
1347 for (final Message msg : messages) {
1348 if (msg.getType() == Message.TYPE_TEXT && msg.getTransferable() == null) {
1349 tmp.add(msg);
1350 }
1351 }
1352 final BigPictureStyle bigPictureStyle = new NotificationCompat.BigPictureStyle();
1353 bigPictureStyle.bigPicture(bitmap);
1354 if (tmp.size() > 0) {
1355 CharSequence text = getMergedBodies(tmp);
1356 bigPictureStyle.setSummaryText(text);
1357 builder.setContentText(text);
1358 builder.setTicker(text);
1359 } else {
1360 final String description =
1361 UIHelper.getFileDescriptionString(mXmppConnectionService, message);
1362 builder.setContentText(description);
1363 builder.setTicker(description);
1364 }
1365 builder.setStyle(bigPictureStyle);
1366 } catch (final IOException e) {
1367 modifyForTextOnly(builder, messages);
1368 }
1369 }
1370
1371 private Person getPerson(Message message) {
1372 final Contact contact = message.getContact();
1373 final Person.Builder builder = new Person.Builder();
1374 if (contact != null) {
1375 builder.setName(contact.getDisplayName());
1376 final Uri uri = contact.getSystemAccount();
1377 if (uri != null) {
1378 builder.setUri(uri.toString());
1379 }
1380 } else {
1381 builder.setName(UIHelper.getMessageDisplayName(message));
1382 }
1383 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
1384 builder.setIcon(
1385 IconCompat.createWithBitmap(
1386 mXmppConnectionService
1387 .getAvatarService()
1388 .get(
1389 message,
1390 AvatarService.getSystemUiAvatarSize(
1391 mXmppConnectionService),
1392 false)));
1393 }
1394 return builder.build();
1395 }
1396
1397 private void modifyForTextOnly(final Builder builder, final ArrayList<Message> messages) {
1398 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
1399 final Conversation conversation = (Conversation) messages.get(0).getConversation();
1400 final Person.Builder meBuilder =
1401 new Person.Builder().setName(mXmppConnectionService.getString(R.string.me));
1402 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
1403 meBuilder.setIcon(
1404 IconCompat.createWithBitmap(
1405 mXmppConnectionService
1406 .getAvatarService()
1407 .get(
1408 conversation.getAccount(),
1409 AvatarService.getSystemUiAvatarSize(
1410 mXmppConnectionService))));
1411 }
1412 final Person me = meBuilder.build();
1413 NotificationCompat.MessagingStyle messagingStyle =
1414 new NotificationCompat.MessagingStyle(me);
1415 final boolean multiple = conversation.getMode() == Conversation.MODE_MULTI;
1416 if (multiple) {
1417 messagingStyle.setConversationTitle(conversation.getName());
1418 }
1419 for (Message message : messages) {
1420 final Person sender =
1421 message.getStatus() == Message.STATUS_RECEIVED ? getPerson(message) : null;
1422 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && isImageMessage(message)) {
1423 final Uri dataUri =
1424 FileBackend.getMediaUri(
1425 mXmppConnectionService,
1426 mXmppConnectionService.getFileBackend().getFile(message));
1427 NotificationCompat.MessagingStyle.Message imageMessage =
1428 new NotificationCompat.MessagingStyle.Message(
1429 UIHelper.getMessagePreview(mXmppConnectionService, message)
1430 .first,
1431 message.getTimeSent(),
1432 sender);
1433 if (dataUri != null) {
1434 imageMessage.setData(message.getMimeType(), dataUri);
1435 }
1436 messagingStyle.addMessage(imageMessage);
1437 } else {
1438 messagingStyle.addMessage(
1439 UIHelper.getMessagePreview(mXmppConnectionService, message).first,
1440 message.getTimeSent(),
1441 sender);
1442 }
1443 }
1444 messagingStyle.setGroupConversation(multiple);
1445 builder.setStyle(messagingStyle);
1446 } else {
1447 if (messages.get(0).getConversation().getMode() == Conversation.MODE_SINGLE) {
1448 builder.setStyle(
1449 new NotificationCompat.BigTextStyle().bigText(getMergedBodies(messages)));
1450 final CharSequence preview =
1451 UIHelper.getMessagePreview(
1452 mXmppConnectionService, messages.get(messages.size() - 1))
1453 .first;
1454 builder.setContentText(preview);
1455 builder.setTicker(preview);
1456 builder.setNumber(messages.size());
1457 } else {
1458 final NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle();
1459 SpannableString styledString;
1460 for (Message message : messages) {
1461 final String name = UIHelper.getMessageDisplayName(message);
1462 styledString = new SpannableString(name + ": " + message.getBody());
1463 styledString.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), 0);
1464 style.addLine(styledString);
1465 }
1466 builder.setStyle(style);
1467 int count = messages.size();
1468 if (count == 1) {
1469 final String name = UIHelper.getMessageDisplayName(messages.get(0));
1470 styledString = new SpannableString(name + ": " + messages.get(0).getBody());
1471 styledString.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), 0);
1472 builder.setContentText(styledString);
1473 builder.setTicker(styledString);
1474 } else {
1475 final String text =
1476 mXmppConnectionService
1477 .getResources()
1478 .getQuantityString(R.plurals.x_messages, count, count);
1479 builder.setContentText(text);
1480 builder.setTicker(text);
1481 }
1482 }
1483 }
1484 }
1485
1486 private Message getImage(final Iterable<Message> messages) {
1487 Message image = null;
1488 for (final Message message : messages) {
1489 if (message.getStatus() != Message.STATUS_RECEIVED) {
1490 return null;
1491 }
1492 if (isImageMessage(message)) {
1493 image = message;
1494 }
1495 }
1496 return image;
1497 }
1498
1499 private Message getFirstDownloadableMessage(final Iterable<Message> messages) {
1500 for (final Message message : messages) {
1501 if (message.getTransferable() != null
1502 || (message.getType() == Message.TYPE_TEXT && message.treatAsDownloadable())) {
1503 return message;
1504 }
1505 }
1506 return null;
1507 }
1508
1509 private Message getFirstLocationMessage(final Iterable<Message> messages) {
1510 for (final Message message : messages) {
1511 if (message.isGeoUri()) {
1512 return message;
1513 }
1514 }
1515 return null;
1516 }
1517
1518 private CharSequence getMergedBodies(final ArrayList<Message> messages) {
1519 final StringBuilder text = new StringBuilder();
1520 for (Message message : messages) {
1521 if (text.length() != 0) {
1522 text.append("\n");
1523 }
1524 text.append(UIHelper.getMessagePreview(mXmppConnectionService, message).first);
1525 }
1526 return text.toString();
1527 }
1528
1529 private PendingIntent createShowLocationIntent(final Message message) {
1530 Iterable<Intent> intents =
1531 GeoHelper.createGeoIntentsFromMessage(mXmppConnectionService, message);
1532 for (final Intent intent : intents) {
1533 if (intent.resolveActivity(mXmppConnectionService.getPackageManager()) != null) {
1534 return PendingIntent.getActivity(
1535 mXmppConnectionService,
1536 generateRequestCode(message.getConversation(), 18),
1537 intent,
1538 s()
1539 ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1540 : PendingIntent.FLAG_UPDATE_CURRENT);
1541 }
1542 }
1543 return null;
1544 }
1545
1546 private PendingIntent createContentIntent(
1547 final String conversationUuid, final String downloadMessageUuid) {
1548 final Intent viewConversationIntent =
1549 new Intent(mXmppConnectionService, ConversationsActivity.class);
1550 viewConversationIntent.setAction(ConversationsActivity.ACTION_VIEW_CONVERSATION);
1551 viewConversationIntent.putExtra(ConversationsActivity.EXTRA_CONVERSATION, conversationUuid);
1552 if (downloadMessageUuid != null) {
1553 viewConversationIntent.putExtra(
1554 ConversationsActivity.EXTRA_DOWNLOAD_UUID, downloadMessageUuid);
1555 return PendingIntent.getActivity(
1556 mXmppConnectionService,
1557 generateRequestCode(conversationUuid, 8),
1558 viewConversationIntent,
1559 s()
1560 ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1561 : PendingIntent.FLAG_UPDATE_CURRENT);
1562 } else {
1563 return PendingIntent.getActivity(
1564 mXmppConnectionService,
1565 generateRequestCode(conversationUuid, 10),
1566 viewConversationIntent,
1567 s()
1568 ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1569 : PendingIntent.FLAG_UPDATE_CURRENT);
1570 }
1571 }
1572
1573 private int generateRequestCode(String uuid, int actionId) {
1574 return (actionId * NOTIFICATION_ID_MULTIPLIER)
1575 + (uuid.hashCode() % NOTIFICATION_ID_MULTIPLIER);
1576 }
1577
1578 private int generateRequestCode(Conversational conversation, int actionId) {
1579 return generateRequestCode(conversation.getUuid(), actionId);
1580 }
1581
1582 private PendingIntent createDownloadIntent(final Message message) {
1583 return createContentIntent(message.getConversationUuid(), message.getUuid());
1584 }
1585
1586 private PendingIntent createContentIntent(final Conversational conversation) {
1587 return createContentIntent(conversation.getUuid(), null);
1588 }
1589
1590 private PendingIntent createDeleteIntent(final Conversation conversation) {
1591 final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
1592 intent.setAction(XmppConnectionService.ACTION_CLEAR_MESSAGE_NOTIFICATION);
1593 if (conversation != null) {
1594 intent.putExtra("uuid", conversation.getUuid());
1595 return PendingIntent.getService(
1596 mXmppConnectionService,
1597 generateRequestCode(conversation, 20),
1598 intent,
1599 s()
1600 ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1601 : PendingIntent.FLAG_UPDATE_CURRENT);
1602 }
1603 return PendingIntent.getService(
1604 mXmppConnectionService,
1605 0,
1606 intent,
1607 s()
1608 ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1609 : PendingIntent.FLAG_UPDATE_CURRENT);
1610 }
1611
1612 private PendingIntent createMissedCallsDeleteIntent(final Conversational conversation) {
1613 final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
1614 intent.setAction(XmppConnectionService.ACTION_CLEAR_MISSED_CALL_NOTIFICATION);
1615 if (conversation != null) {
1616 intent.putExtra("uuid", conversation.getUuid());
1617 return PendingIntent.getService(
1618 mXmppConnectionService,
1619 generateRequestCode(conversation, 21),
1620 intent,
1621 s()
1622 ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1623 : PendingIntent.FLAG_UPDATE_CURRENT);
1624 }
1625 return PendingIntent.getService(
1626 mXmppConnectionService,
1627 1,
1628 intent,
1629 s()
1630 ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1631 : PendingIntent.FLAG_UPDATE_CURRENT);
1632 }
1633
1634 private PendingIntent createReplyIntent(
1635 final Conversation conversation,
1636 final String lastMessageUuid,
1637 final boolean dismissAfterReply) {
1638 final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
1639 intent.setAction(XmppConnectionService.ACTION_REPLY_TO_CONVERSATION);
1640 intent.putExtra("uuid", conversation.getUuid());
1641 intent.putExtra("dismiss_notification", dismissAfterReply);
1642 intent.putExtra("last_message_uuid", lastMessageUuid);
1643 final int id = generateRequestCode(conversation, dismissAfterReply ? 12 : 14);
1644 return PendingIntent.getService(
1645 mXmppConnectionService,
1646 id,
1647 intent,
1648 s()
1649 ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1650 : PendingIntent.FLAG_UPDATE_CURRENT);
1651 }
1652
1653 private PendingIntent createReadPendingIntent(Conversation conversation) {
1654 final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
1655 intent.setAction(XmppConnectionService.ACTION_MARK_AS_READ);
1656 intent.putExtra("uuid", conversation.getUuid());
1657 intent.setPackage(mXmppConnectionService.getPackageName());
1658 return PendingIntent.getService(
1659 mXmppConnectionService,
1660 generateRequestCode(conversation, 16),
1661 intent,
1662 s()
1663 ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1664 : PendingIntent.FLAG_UPDATE_CURRENT);
1665 }
1666
1667 private PendingIntent createCallAction(String sessionId, final String action, int requestCode) {
1668 return pendingServiceIntent(mXmppConnectionService, action, requestCode, ImmutableMap.of(RtpSessionActivity.EXTRA_SESSION_ID, sessionId));
1669 }
1670
1671 private PendingIntent createSnoozeIntent(final Conversation conversation) {
1672 return pendingServiceIntent(mXmppConnectionService, XmppConnectionService.ACTION_SNOOZE, generateRequestCode(conversation,22),ImmutableMap.of("uuid",conversation.getUuid()));
1673 }
1674
1675 private static PendingIntent pendingServiceIntent(final Context context, final String action, final int requestCode) {
1676 return pendingServiceIntent(context, action, requestCode, ImmutableMap.of());
1677 }
1678
1679 private static PendingIntent pendingServiceIntent(final Context context, final String action, final int requestCode, final Map<String,String> extras) {
1680 final Intent intent = new Intent(context, XmppConnectionService.class);
1681 intent.setAction(action);
1682 for(final Map.Entry<String,String> entry : extras.entrySet()) {
1683 intent.putExtra(entry.getKey(), entry.getValue());
1684 }
1685 return PendingIntent.getService(
1686 context,
1687 requestCode,
1688 intent,
1689 s()
1690 ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1691 : PendingIntent.FLAG_UPDATE_CURRENT);
1692 }
1693
1694 private boolean wasHighlightedOrPrivate(final Message message) {
1695 if (message.getConversation() instanceof Conversation) {
1696 Conversation conversation = (Conversation) message.getConversation();
1697 final String nick = conversation.getMucOptions().getActualNick();
1698 final Pattern highlight = generateNickHighlightPattern(nick);
1699 if (message.getBody() == null || nick == null) {
1700 return false;
1701 }
1702 final Matcher m = highlight.matcher(message.getBody());
1703 return (m.find() || message.isPrivateMessage());
1704 } else {
1705 return false;
1706 }
1707 }
1708
1709 public void setOpenConversation(final Conversation conversation) {
1710 this.mOpenConversation = conversation;
1711 }
1712
1713 public void setIsInForeground(final boolean foreground) {
1714 this.mIsInForeground = foreground;
1715 }
1716
1717 private int getPixel(final int dp) {
1718 final DisplayMetrics metrics = mXmppConnectionService.getResources().getDisplayMetrics();
1719 return ((int) (dp * metrics.density));
1720 }
1721
1722 private void markLastNotification() {
1723 this.mLastNotification = SystemClock.elapsedRealtime();
1724 }
1725
1726 private boolean inMiniGracePeriod(final Account account) {
1727 final int miniGrace =
1728 account.getStatus() == Account.State.ONLINE
1729 ? Config.MINI_GRACE_PERIOD
1730 : Config.MINI_GRACE_PERIOD * 2;
1731 return SystemClock.elapsedRealtime() < (this.mLastNotification + miniGrace);
1732 }
1733
1734 Notification createForegroundNotification() {
1735 final Notification.Builder mBuilder = new Notification.Builder(mXmppConnectionService);
1736 mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.app_name));
1737 final List<Account> accounts = mXmppConnectionService.getAccounts();
1738 final int enabled;
1739 final int connected;
1740 if (accounts == null) {
1741 enabled = 0;
1742 connected = 0;
1743 } else {
1744 enabled = Iterables.size(Iterables.filter(accounts, Account::isEnabled));
1745 connected =
1746 Iterables.size(Iterables.filter(accounts, Account::isOnlineAndConnected));
1747 }
1748 mBuilder.setContentText(
1749 mXmppConnectionService.getString(R.string.connected_accounts, connected, enabled));
1750 final PendingIntent openIntent = createOpenConversationsIntent();
1751 if (openIntent != null) {
1752 mBuilder.setContentIntent(openIntent);
1753 }
1754 mBuilder.setWhen(0)
1755 .setPriority(Notification.PRIORITY_MIN)
1756 .setSmallIcon(
1757 connected > 0
1758 ? R.drawable.ic_link_white_24dp
1759 : R.drawable.ic_link_off_white_24dp)
1760 .setLocalOnly(true);
1761
1762 if (Compatibility.runsTwentySix()) {
1763 mBuilder.setChannelId("foreground");
1764 mBuilder.addAction(
1765 R.drawable.ic_logout_white_24dp,
1766 mXmppConnectionService.getString(R.string.log_out),
1767 pendingServiceIntent(
1768 mXmppConnectionService,
1769 XmppConnectionService.ACTION_TEMPORARILY_DISABLE,
1770 87));
1771 mBuilder.addAction(
1772 R.drawable.ic_notifications_off_white_24dp,
1773 mXmppConnectionService.getString(R.string.hide_notification),
1774 pendingNotificationSettingsIntent(mXmppConnectionService));
1775 }
1776
1777 return mBuilder.build();
1778 }
1779
1780 @RequiresApi(api = Build.VERSION_CODES.O)
1781 private static PendingIntent pendingNotificationSettingsIntent(final Context context) {
1782 final Intent intent = new Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS);
1783 intent.putExtra(Settings.EXTRA_APP_PACKAGE, context.getPackageName());
1784 intent.putExtra(Settings.EXTRA_CHANNEL_ID, "foreground");
1785 return PendingIntent.getActivity(
1786 context,
1787 89,
1788 intent,
1789 s()
1790 ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1791 : PendingIntent.FLAG_UPDATE_CURRENT);
1792 }
1793
1794 private PendingIntent createOpenConversationsIntent() {
1795 try {
1796 return PendingIntent.getActivity(
1797 mXmppConnectionService,
1798 0,
1799 new Intent(mXmppConnectionService, ConversationsActivity.class),
1800 s()
1801 ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1802 : PendingIntent.FLAG_UPDATE_CURRENT);
1803 } catch (RuntimeException e) {
1804 return null;
1805 }
1806 }
1807
1808 void updateErrorNotification() {
1809 if (Config.SUPPRESS_ERROR_NOTIFICATION) {
1810 cancel(ERROR_NOTIFICATION_ID);
1811 return;
1812 }
1813 final boolean showAllErrors = QuickConversationsService.isConversations();
1814 final List<Account> errors = new ArrayList<>();
1815 boolean torNotAvailable = false;
1816 for (final Account account : mXmppConnectionService.getAccounts()) {
1817 if (account.hasErrorStatus()
1818 && account.showErrorNotification()
1819 && (showAllErrors
1820 || account.getLastErrorStatus() == Account.State.UNAUTHORIZED)) {
1821 errors.add(account);
1822 torNotAvailable |= account.getStatus() == Account.State.TOR_NOT_AVAILABLE;
1823 }
1824 }
1825 if (mXmppConnectionService.foregroundNotificationNeedsUpdatingWhenErrorStateChanges()) {
1826 notify(FOREGROUND_NOTIFICATION_ID, createForegroundNotification());
1827 }
1828 final Notification.Builder mBuilder = new Notification.Builder(mXmppConnectionService);
1829 if (errors.size() == 0) {
1830 cancel(ERROR_NOTIFICATION_ID);
1831 return;
1832 } else if (errors.size() == 1) {
1833 mBuilder.setContentTitle(
1834 mXmppConnectionService.getString(R.string.problem_connecting_to_account));
1835 mBuilder.setContentText(errors.get(0).getJid().asBareJid().toEscapedString());
1836 } else {
1837 mBuilder.setContentTitle(
1838 mXmppConnectionService.getString(R.string.problem_connecting_to_accounts));
1839 mBuilder.setContentText(mXmppConnectionService.getString(R.string.touch_to_fix));
1840 }
1841 mBuilder.addAction(
1842 R.drawable.ic_autorenew_white_24dp,
1843 mXmppConnectionService.getString(R.string.try_again),
1844 pendingServiceIntent(mXmppConnectionService, XmppConnectionService.ACTION_TRY_AGAIN, 45));
1845 if (torNotAvailable) {
1846 if (TorServiceUtils.isOrbotInstalled(mXmppConnectionService)) {
1847 mBuilder.addAction(
1848 R.drawable.ic_play_circle_filled_white_48dp,
1849 mXmppConnectionService.getString(R.string.start_orbot),
1850 PendingIntent.getActivity(
1851 mXmppConnectionService,
1852 147,
1853 TorServiceUtils.LAUNCH_INTENT,
1854 s()
1855 ? PendingIntent.FLAG_IMMUTABLE
1856 | PendingIntent.FLAG_UPDATE_CURRENT
1857 : PendingIntent.FLAG_UPDATE_CURRENT));
1858 } else {
1859 mBuilder.addAction(
1860 R.drawable.ic_file_download_white_24dp,
1861 mXmppConnectionService.getString(R.string.install_orbot),
1862 PendingIntent.getActivity(
1863 mXmppConnectionService,
1864 146,
1865 TorServiceUtils.INSTALL_INTENT,
1866 s()
1867 ? PendingIntent.FLAG_IMMUTABLE
1868 | PendingIntent.FLAG_UPDATE_CURRENT
1869 : PendingIntent.FLAG_UPDATE_CURRENT));
1870 }
1871 }
1872 mBuilder.setDeleteIntent(pendingServiceIntent(mXmppConnectionService,XmppConnectionService.ACTION_DISMISS_ERROR_NOTIFICATIONS, 69));
1873 mBuilder.setVisibility(Notification.VISIBILITY_PRIVATE);
1874 mBuilder.setSmallIcon(R.drawable.ic_warning_white_24dp);
1875 mBuilder.setLocalOnly(true);
1876 mBuilder.setPriority(Notification.PRIORITY_LOW);
1877 final Intent intent;
1878 if (AccountUtils.MANAGE_ACCOUNT_ACTIVITY != null) {
1879 intent = new Intent(mXmppConnectionService, AccountUtils.MANAGE_ACCOUNT_ACTIVITY);
1880 } else {
1881 intent = new Intent(mXmppConnectionService, EditAccountActivity.class);
1882 intent.putExtra("jid", errors.get(0).getJid().asBareJid().toEscapedString());
1883 intent.putExtra(EditAccountActivity.EXTRA_OPENED_FROM_NOTIFICATION, true);
1884 }
1885 mBuilder.setContentIntent(
1886 PendingIntent.getActivity(
1887 mXmppConnectionService,
1888 145,
1889 intent,
1890 s()
1891 ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1892 : PendingIntent.FLAG_UPDATE_CURRENT));
1893 if (Compatibility.runsTwentySix()) {
1894 mBuilder.setChannelId("error");
1895 }
1896 notify(ERROR_NOTIFICATION_ID, mBuilder.build());
1897 }
1898
1899 void updateFileAddingNotification(int current, Message message) {
1900 Notification.Builder mBuilder = new Notification.Builder(mXmppConnectionService);
1901 mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.transcoding_video));
1902 mBuilder.setProgress(100, current, false);
1903 mBuilder.setSmallIcon(R.drawable.ic_hourglass_empty_white_24dp);
1904 mBuilder.setContentIntent(createContentIntent(message.getConversation()));
1905 mBuilder.setOngoing(true);
1906 if (Compatibility.runsTwentySix()) {
1907 mBuilder.setChannelId("compression");
1908 }
1909 Notification notification = mBuilder.build();
1910 notify(FOREGROUND_NOTIFICATION_ID, notification);
1911 }
1912
1913 private void notify(String tag, int id, Notification notification) {
1914 final NotificationManagerCompat notificationManager =
1915 NotificationManagerCompat.from(mXmppConnectionService);
1916 try {
1917 notificationManager.notify(tag, id, notification);
1918 } catch (RuntimeException e) {
1919 Log.d(Config.LOGTAG, "unable to make notification", e);
1920 }
1921 }
1922
1923 public void notify(int id, Notification notification) {
1924 final NotificationManagerCompat notificationManager =
1925 NotificationManagerCompat.from(mXmppConnectionService);
1926 try {
1927 notificationManager.notify(id, notification);
1928 } catch (RuntimeException e) {
1929 Log.d(Config.LOGTAG, "unable to make notification", e);
1930 }
1931 }
1932
1933 public void cancel(int id) {
1934 final NotificationManagerCompat notificationManager =
1935 NotificationManagerCompat.from(mXmppConnectionService);
1936 try {
1937 notificationManager.cancel(id);
1938 } catch (RuntimeException e) {
1939 Log.d(Config.LOGTAG, "unable to cancel notification", e);
1940 }
1941 }
1942
1943 private void cancel(String tag, int id) {
1944 final NotificationManagerCompat notificationManager =
1945 NotificationManagerCompat.from(mXmppConnectionService);
1946 try {
1947 notificationManager.cancel(tag, id);
1948 } catch (RuntimeException e) {
1949 Log.d(Config.LOGTAG, "unable to cancel notification", e);
1950 }
1951 }
1952
1953 private static class MissedCallsInfo {
1954 private int numberOfCalls;
1955 private long lastTime;
1956
1957 MissedCallsInfo(final long time) {
1958 numberOfCalls = 1;
1959 lastTime = time;
1960 }
1961
1962 public void newMissedCall(final long time) {
1963 ++numberOfCalls;
1964 lastTime = time;
1965 }
1966
1967 public boolean removeMissedCall() {
1968 --numberOfCalls;
1969 return numberOfCalls <= 0;
1970 }
1971
1972 public int getNumberOfCalls() {
1973 return numberOfCalls;
1974 }
1975
1976 public long getLastTime() {
1977 return lastTime;
1978 }
1979 }
1980
1981 private class VibrationRunnable implements Runnable {
1982
1983 @Override
1984 public void run() {
1985 final Vibrator vibrator =
1986 (Vibrator) mXmppConnectionService.getSystemService(Context.VIBRATOR_SERVICE);
1987 vibrator.vibrate(CALL_PATTERN, -1);
1988 }
1989 }
1990}