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