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