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