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