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