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