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 builder.setLocalOnly(true);
646 return builder.build();
647 }
648
649 private PendingIntent createPendingRtpSession(
650 final AbstractJingleConnection.Id id, final String action, final int requestCode) {
651 final Intent fullScreenIntent =
652 new Intent(mXmppConnectionService, RtpSessionActivity.class);
653 fullScreenIntent.setAction(action);
654 fullScreenIntent.putExtra(
655 RtpSessionActivity.EXTRA_ACCOUNT,
656 id.account.getJid().asBareJid().toEscapedString());
657 fullScreenIntent.putExtra(RtpSessionActivity.EXTRA_WITH, id.with.toEscapedString());
658 fullScreenIntent.putExtra(RtpSessionActivity.EXTRA_SESSION_ID, id.sessionId);
659 return PendingIntent.getActivity(
660 mXmppConnectionService,
661 requestCode,
662 fullScreenIntent,
663 s()
664 ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
665 : PendingIntent.FLAG_UPDATE_CURRENT);
666 }
667
668 public void cancelIncomingCallNotification() {
669 stopSoundAndVibration();
670 cancel(INCOMING_CALL_NOTIFICATION_ID);
671 }
672
673 public boolean stopSoundAndVibration() {
674 int stopped = 0;
675 if (this.currentlyPlayingRingtone != null) {
676 if (this.currentlyPlayingRingtone.isPlaying()) {
677 Log.d(Config.LOGTAG, "stop playing ring tone");
678 ++stopped;
679 }
680 this.currentlyPlayingRingtone.stop();
681 }
682 if (this.vibrationFuture != null && !this.vibrationFuture.isCancelled()) {
683 Log.d(Config.LOGTAG, "stop vibration");
684 this.vibrationFuture.cancel(true);
685 ++stopped;
686 }
687 return stopped > 0;
688 }
689
690 public static void cancelIncomingCallNotification(final Context context) {
691 final NotificationManagerCompat notificationManager =
692 NotificationManagerCompat.from(context);
693 try {
694 notificationManager.cancel(INCOMING_CALL_NOTIFICATION_ID);
695 } catch (RuntimeException e) {
696 Log.d(Config.LOGTAG, "unable to cancel incoming call notification after crash", e);
697 }
698 }
699
700 private void pushNow(final Message message) {
701 mXmppConnectionService.updateUnreadCountBadge();
702 if (!notifyMessage(message)) {
703 Log.d(
704 Config.LOGTAG,
705 message.getConversation().getAccount().getJid().asBareJid()
706 + ": suppressing notification because turned off");
707 return;
708 }
709 final boolean isScreenLocked = mXmppConnectionService.isScreenLocked();
710 if (this.mIsInForeground
711 && !isScreenLocked
712 && this.mOpenConversation == message.getConversation()) {
713 Log.d(
714 Config.LOGTAG,
715 message.getConversation().getAccount().getJid().asBareJid()
716 + ": suppressing notification because conversation is open");
717 return;
718 }
719 synchronized (notifications) {
720 pushToStack(message);
721 final Conversational conversation = message.getConversation();
722 final Account account = conversation.getAccount();
723 final boolean doNotify =
724 (!(this.mIsInForeground && this.mOpenConversation == null) || isScreenLocked)
725 && !account.inGracePeriod()
726 && !this.inMiniGracePeriod(account);
727 updateNotification(doNotify, Collections.singletonList(conversation.getUuid()));
728 }
729 }
730
731 private void pushMissedCall(final Message message) {
732 final Conversational conversation = message.getConversation();
733 final MissedCallsInfo info = mMissedCalls.get(conversation);
734 if (info == null) {
735 mMissedCalls.put(conversation, new MissedCallsInfo(message.getTimeSent()));
736 } else {
737 info.newMissedCall(message.getTimeSent());
738 }
739 }
740
741 public void pushMissedCallNow(final Message message) {
742 synchronized (mMissedCalls) {
743 pushMissedCall(message);
744 updateMissedCallNotifications(Collections.singleton(message.getConversation()));
745 }
746 }
747
748 public void clear(final Conversation conversation) {
749 clearMessages(conversation);
750 clearMissedCalls(conversation);
751 }
752
753 public void clearMessages() {
754 synchronized (notifications) {
755 for (ArrayList<Message> messages : notifications.values()) {
756 markAsReadIfHasDirectReply(messages);
757 }
758 notifications.clear();
759 updateNotification(false);
760 }
761 }
762
763 public void clearMessages(final Conversation conversation) {
764 synchronized (this.mBacklogMessageCounter) {
765 this.mBacklogMessageCounter.remove(conversation);
766 }
767 synchronized (notifications) {
768 markAsReadIfHasDirectReply(conversation);
769 if (notifications.remove(conversation.getUuid()) != null) {
770 cancel(conversation.getUuid(), NOTIFICATION_ID);
771 updateNotification(false, null, true);
772 }
773 }
774 }
775
776 public void clearMissedCall(final Message message) {
777 synchronized (mMissedCalls) {
778 final Iterator<Map.Entry<Conversational,MissedCallsInfo>> iterator = mMissedCalls.entrySet().iterator();
779 while (iterator.hasNext()) {
780 final Map.Entry<Conversational, MissedCallsInfo> entry = iterator.next();
781 final Conversational conversational = entry.getKey();
782 final MissedCallsInfo missedCallsInfo = entry.getValue();
783 if (conversational.getUuid().equals(message.getConversation().getUuid())) {
784 if (missedCallsInfo.removeMissedCall()) {
785 cancel(conversational.getUuid(), MISSED_CALL_NOTIFICATION_ID);
786 Log.d(Config.LOGTAG,conversational.getAccount().getJid().asBareJid()+": dismissed missed call because call was picked up on other device");
787 iterator.remove();
788 }
789 }
790 }
791 updateMissedCallNotifications(null);
792 }
793 }
794
795 public void clearMissedCalls() {
796 synchronized (mMissedCalls) {
797 for (final Conversational conversation : mMissedCalls.keySet()) {
798 cancel(conversation.getUuid(), MISSED_CALL_NOTIFICATION_ID);
799 }
800 mMissedCalls.clear();
801 updateMissedCallNotifications(null);
802 }
803 }
804
805 public void clearMissedCalls(final Conversation conversation) {
806 synchronized (mMissedCalls) {
807 if (mMissedCalls.remove(conversation) != null) {
808 cancel(conversation.getUuid(), MISSED_CALL_NOTIFICATION_ID);
809 updateMissedCallNotifications(null);
810 }
811 }
812 }
813
814 private void markAsReadIfHasDirectReply(final Conversation conversation) {
815 markAsReadIfHasDirectReply(notifications.get(conversation.getUuid()));
816 }
817
818 private void markAsReadIfHasDirectReply(final ArrayList<Message> messages) {
819 if (messages != null && messages.size() > 0) {
820 Message last = messages.get(messages.size() - 1);
821 if (last.getStatus() != Message.STATUS_RECEIVED) {
822 if (mXmppConnectionService.markRead((Conversation) last.getConversation(), false)) {
823 mXmppConnectionService.updateConversationUi();
824 }
825 }
826 }
827 }
828
829 private void setNotificationColor(final Builder mBuilder) {
830 mBuilder.setColor(ContextCompat.getColor(mXmppConnectionService, R.color.green600));
831 }
832
833 public void updateNotification() {
834 synchronized (notifications) {
835 updateNotification(false);
836 }
837 }
838
839 private void updateNotification(final boolean notify) {
840 updateNotification(notify, null, false);
841 }
842
843 private void updateNotification(final boolean notify, final List<String> conversations) {
844 updateNotification(notify, conversations, false);
845 }
846
847 private void updateNotification(
848 final boolean notify, final List<String> conversations, final boolean summaryOnly) {
849 final SharedPreferences preferences =
850 PreferenceManager.getDefaultSharedPreferences(mXmppConnectionService);
851
852 final boolean quiteHours = isQuietHours();
853
854 final boolean notifyOnlyOneChild =
855 notify
856 && conversations != null
857 && conversations.size()
858 == 1; // if this check is changed to > 0 catchup messages will
859 // create one notification per conversation
860
861 if (notifications.size() == 0) {
862 cancel(NOTIFICATION_ID);
863 } else {
864 if (notify) {
865 this.markLastNotification();
866 }
867 final Builder mBuilder;
868 if (notifications.size() == 1 && Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
869 mBuilder =
870 buildSingleConversations(
871 notifications.values().iterator().next(), notify, quiteHours);
872 modifyForSoundVibrationAndLight(mBuilder, notify, quiteHours, preferences);
873 notify(NOTIFICATION_ID, mBuilder.build());
874 } else {
875 mBuilder = buildMultipleConversation(notify, quiteHours);
876 if (notifyOnlyOneChild) {
877 mBuilder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN);
878 }
879 modifyForSoundVibrationAndLight(mBuilder, notify, quiteHours, preferences);
880 if (!summaryOnly) {
881 for (Map.Entry<String, ArrayList<Message>> entry : notifications.entrySet()) {
882 String uuid = entry.getKey();
883 final boolean notifyThis =
884 notifyOnlyOneChild ? conversations.contains(uuid) : notify;
885 Builder singleBuilder =
886 buildSingleConversations(entry.getValue(), notifyThis, quiteHours);
887 if (!notifyOnlyOneChild) {
888 singleBuilder.setGroupAlertBehavior(
889 NotificationCompat.GROUP_ALERT_SUMMARY);
890 }
891 modifyForSoundVibrationAndLight(
892 singleBuilder, notifyThis, quiteHours, preferences);
893 singleBuilder.setGroup(MESSAGES_GROUP);
894 setNotificationColor(singleBuilder);
895 notify(entry.getKey(), NOTIFICATION_ID, singleBuilder.build());
896 }
897 }
898 notify(NOTIFICATION_ID, mBuilder.build());
899 }
900 }
901 }
902
903 private void updateMissedCallNotifications(final Set<Conversational> update) {
904 if (mMissedCalls.isEmpty()) {
905 cancel(MISSED_CALL_NOTIFICATION_ID);
906 return;
907 }
908 if (mMissedCalls.size() == 1 && Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
909 final Conversational conversation = mMissedCalls.keySet().iterator().next();
910 final MissedCallsInfo info = mMissedCalls.values().iterator().next();
911 final Notification notification = missedCall(conversation, info);
912 notify(MISSED_CALL_NOTIFICATION_ID, notification);
913 } else {
914 final Notification summary = missedCallsSummary();
915 notify(MISSED_CALL_NOTIFICATION_ID, summary);
916 if (update != null) {
917 for (final Conversational conversation : update) {
918 final MissedCallsInfo info = mMissedCalls.get(conversation);
919 if (info != null) {
920 final Notification notification = missedCall(conversation, info);
921 notify(conversation.getUuid(), MISSED_CALL_NOTIFICATION_ID, notification);
922 }
923 }
924 }
925 }
926 }
927
928 private void modifyForSoundVibrationAndLight(
929 Builder mBuilder, boolean notify, boolean quietHours, SharedPreferences preferences) {
930 final Resources resources = mXmppConnectionService.getResources();
931 final String ringtone =
932 preferences.getString(
933 "notification_ringtone",
934 resources.getString(R.string.notification_ringtone));
935 final boolean vibrate =
936 preferences.getBoolean(
937 "vibrate_on_notification",
938 resources.getBoolean(R.bool.vibrate_on_notification));
939 final boolean led = preferences.getBoolean("led", resources.getBoolean(R.bool.led));
940 final boolean headsup =
941 preferences.getBoolean(
942 "notification_headsup", resources.getBoolean(R.bool.headsup_notifications));
943 if (notify && !quietHours) {
944 if (vibrate) {
945 final int dat = 70;
946 final long[] pattern = {0, 3 * dat, dat, dat};
947 mBuilder.setVibrate(pattern);
948 } else {
949 mBuilder.setVibrate(new long[] {0});
950 }
951 Uri uri = Uri.parse(ringtone);
952 try {
953 mBuilder.setSound(fixRingtoneUri(uri));
954 } catch (SecurityException e) {
955 Log.d(Config.LOGTAG, "unable to use custom notification sound " + uri.toString());
956 }
957 } else {
958 mBuilder.setLocalOnly(true);
959 }
960 mBuilder.setCategory(Notification.CATEGORY_MESSAGE);
961 mBuilder.setPriority(
962 notify
963 ? (headsup
964 ? NotificationCompat.PRIORITY_HIGH
965 : NotificationCompat.PRIORITY_DEFAULT)
966 : NotificationCompat.PRIORITY_LOW);
967 setNotificationColor(mBuilder);
968 mBuilder.setDefaults(0);
969 if (led) {
970 mBuilder.setLights(LED_COLOR, 2000, 3000);
971 }
972 }
973
974 private void modifyIncomingCall(final Builder mBuilder) {
975 mBuilder.setPriority(NotificationCompat.PRIORITY_HIGH);
976 setNotificationColor(mBuilder);
977 mBuilder.setLights(LED_COLOR, 2000, 3000);
978 }
979
980 private Uri fixRingtoneUri(Uri uri) {
981 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && "file".equals(uri.getScheme())) {
982 return FileBackend.getUriForFile(mXmppConnectionService, new File(uri.getPath()));
983 } else {
984 return uri;
985 }
986 }
987
988 private Notification missedCallsSummary() {
989 final Builder publicBuilder = buildMissedCallsSummary(true);
990 final Builder builder = buildMissedCallsSummary(false);
991 builder.setPublicVersion(publicBuilder.build());
992 return builder.build();
993 }
994
995 private Builder buildMissedCallsSummary(boolean publicVersion) {
996 final Builder builder =
997 new NotificationCompat.Builder(mXmppConnectionService, "missed_calls");
998 int totalCalls = 0;
999 final List<String> names = new ArrayList<>();
1000 long lastTime = 0;
1001 for (final Map.Entry<Conversational, MissedCallsInfo> entry : mMissedCalls.entrySet()) {
1002 final Conversational conversation = entry.getKey();
1003 final MissedCallsInfo missedCallsInfo = entry.getValue();
1004 names.add(conversation.getContact().getDisplayName());
1005 totalCalls += missedCallsInfo.getNumberOfCalls();
1006 lastTime = Math.max(lastTime, missedCallsInfo.getLastTime());
1007 }
1008 final String title =
1009 (totalCalls == 1)
1010 ? mXmppConnectionService.getString(R.string.missed_call)
1011 : (mMissedCalls.size() == 1)
1012 ? mXmppConnectionService
1013 .getResources()
1014 .getQuantityString(
1015 R.plurals.n_missed_calls, totalCalls, totalCalls)
1016 : mXmppConnectionService
1017 .getResources()
1018 .getQuantityString(
1019 R.plurals.n_missed_calls_from_m_contacts,
1020 mMissedCalls.size(),
1021 totalCalls,
1022 mMissedCalls.size());
1023 builder.setContentTitle(title);
1024 builder.setTicker(title);
1025 if (!publicVersion) {
1026 builder.setContentText(Joiner.on(", ").join(names));
1027 }
1028 builder.setSmallIcon(R.drawable.ic_call_missed_white_24db);
1029 builder.setGroupSummary(true);
1030 builder.setGroup(MISSED_CALLS_GROUP);
1031 builder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN);
1032 builder.setCategory(NotificationCompat.CATEGORY_CALL);
1033 builder.setWhen(lastTime);
1034 if (!mMissedCalls.isEmpty()) {
1035 final Conversational firstConversation = mMissedCalls.keySet().iterator().next();
1036 builder.setContentIntent(createContentIntent(firstConversation));
1037 }
1038 builder.setDeleteIntent(createMissedCallsDeleteIntent(null));
1039 modifyMissedCall(builder);
1040 return builder;
1041 }
1042
1043 private Notification missedCall(final Conversational conversation, final MissedCallsInfo info) {
1044 final Builder publicBuilder = buildMissedCall(conversation, info, true);
1045 final Builder builder = buildMissedCall(conversation, info, false);
1046 builder.setPublicVersion(publicBuilder.build());
1047 return builder.build();
1048 }
1049
1050 private Builder buildMissedCall(
1051 final Conversational conversation, final MissedCallsInfo info, boolean publicVersion) {
1052 final Builder builder =
1053 new NotificationCompat.Builder(mXmppConnectionService, "missed_calls");
1054 final String title =
1055 (info.getNumberOfCalls() == 1)
1056 ? mXmppConnectionService.getString(R.string.missed_call)
1057 : mXmppConnectionService
1058 .getResources()
1059 .getQuantityString(
1060 R.plurals.n_missed_calls,
1061 info.getNumberOfCalls(),
1062 info.getNumberOfCalls());
1063 builder.setContentTitle(title);
1064 final String name = conversation.getContact().getDisplayName();
1065 if (publicVersion) {
1066 builder.setTicker(title);
1067 } else {
1068 builder.setTicker(
1069 mXmppConnectionService
1070 .getResources()
1071 .getQuantityString(
1072 R.plurals.n_missed_calls_from_x,
1073 info.getNumberOfCalls(),
1074 info.getNumberOfCalls(),
1075 name));
1076 builder.setContentText(name);
1077 }
1078 builder.setSmallIcon(R.drawable.ic_call_missed_white_24db);
1079 builder.setGroup(MISSED_CALLS_GROUP);
1080 builder.setCategory(NotificationCompat.CATEGORY_CALL);
1081 builder.setWhen(info.getLastTime());
1082 builder.setContentIntent(createContentIntent(conversation));
1083 builder.setDeleteIntent(createMissedCallsDeleteIntent(conversation));
1084 if (!publicVersion && conversation instanceof Conversation) {
1085 builder.setLargeIcon(
1086 mXmppConnectionService
1087 .getAvatarService()
1088 .get(
1089 (Conversation) conversation,
1090 AvatarService.getSystemUiAvatarSize(mXmppConnectionService)));
1091 }
1092 modifyMissedCall(builder);
1093 return builder;
1094 }
1095
1096 private void modifyMissedCall(final Builder builder) {
1097 final SharedPreferences preferences =
1098 PreferenceManager.getDefaultSharedPreferences(mXmppConnectionService);
1099 final Resources resources = mXmppConnectionService.getResources();
1100 final boolean led = preferences.getBoolean("led", resources.getBoolean(R.bool.led));
1101 if (led) {
1102 builder.setLights(LED_COLOR, 2000, 3000);
1103 }
1104 builder.setPriority(NotificationCompat.PRIORITY_HIGH);
1105 builder.setSound(null);
1106 setNotificationColor(builder);
1107 }
1108
1109 private Builder buildMultipleConversation(final boolean notify, final boolean quietHours) {
1110 final Builder mBuilder =
1111 new NotificationCompat.Builder(
1112 mXmppConnectionService,
1113 quietHours ? "quiet_hours" : (notify ? "messages" : "silent_messages"));
1114 final NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle();
1115 style.setBigContentTitle(
1116 mXmppConnectionService
1117 .getResources()
1118 .getQuantityString(
1119 R.plurals.x_unread_conversations,
1120 notifications.size(),
1121 notifications.size()));
1122 final List<String> names = new ArrayList<>();
1123 Conversation conversation = null;
1124 for (final ArrayList<Message> messages : notifications.values()) {
1125 if (messages.isEmpty()) {
1126 continue;
1127 }
1128 conversation = (Conversation) messages.get(0).getConversation();
1129 final String name = conversation.getName().toString();
1130 SpannableString styledString;
1131 if (Config.HIDE_MESSAGE_TEXT_IN_NOTIFICATION) {
1132 int count = messages.size();
1133 styledString =
1134 new SpannableString(
1135 name
1136 + ": "
1137 + mXmppConnectionService
1138 .getResources()
1139 .getQuantityString(
1140 R.plurals.x_messages, count, count));
1141 styledString.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), 0);
1142 style.addLine(styledString);
1143 } else {
1144 styledString =
1145 new SpannableString(
1146 name
1147 + ": "
1148 + UIHelper.getMessagePreview(
1149 mXmppConnectionService, messages.get(0))
1150 .first);
1151 styledString.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), 0);
1152 style.addLine(styledString);
1153 }
1154 names.add(name);
1155 }
1156 final String contentTitle =
1157 mXmppConnectionService
1158 .getResources()
1159 .getQuantityString(
1160 R.plurals.x_unread_conversations,
1161 notifications.size(),
1162 notifications.size());
1163 mBuilder.setContentTitle(contentTitle);
1164 mBuilder.setTicker(contentTitle);
1165 mBuilder.setContentText(Joiner.on(", ").join(names));
1166 mBuilder.setStyle(style);
1167 if (conversation != null) {
1168 mBuilder.setContentIntent(createContentIntent(conversation));
1169 }
1170 mBuilder.setGroupSummary(true);
1171 mBuilder.setGroup(MESSAGES_GROUP);
1172 mBuilder.setDeleteIntent(createDeleteIntent(null));
1173 mBuilder.setSmallIcon(R.drawable.ic_notification);
1174 return mBuilder;
1175 }
1176
1177 private Builder buildSingleConversations(
1178 final ArrayList<Message> messages, final boolean notify, final boolean quietHours) {
1179 final Builder mBuilder =
1180 new NotificationCompat.Builder(
1181 mXmppConnectionService,
1182 quietHours ? "quiet_hours" : (notify ? "messages" : "silent_messages"));
1183 if (messages.size() >= 1) {
1184 final Conversation conversation = (Conversation) messages.get(0).getConversation();
1185 mBuilder.setLargeIcon(
1186 mXmppConnectionService
1187 .getAvatarService()
1188 .get(
1189 conversation,
1190 AvatarService.getSystemUiAvatarSize(mXmppConnectionService)));
1191 mBuilder.setContentTitle(conversation.getName());
1192 if (Config.HIDE_MESSAGE_TEXT_IN_NOTIFICATION) {
1193 int count = messages.size();
1194 mBuilder.setContentText(
1195 mXmppConnectionService
1196 .getResources()
1197 .getQuantityString(R.plurals.x_messages, count, count));
1198 } else {
1199 Message message;
1200 // TODO starting with Android 9 we might want to put images in MessageStyle
1201 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P
1202 && (message = getImage(messages)) != null) {
1203 modifyForImage(mBuilder, message, messages);
1204 } else {
1205 modifyForTextOnly(mBuilder, messages);
1206 }
1207 RemoteInput remoteInput =
1208 new RemoteInput.Builder("text_reply")
1209 .setLabel(
1210 UIHelper.getMessageHint(
1211 mXmppConnectionService, conversation))
1212 .build();
1213 PendingIntent markAsReadPendingIntent = createReadPendingIntent(conversation);
1214 NotificationCompat.Action markReadAction =
1215 new NotificationCompat.Action.Builder(
1216 R.drawable.ic_drafts_white_24dp,
1217 mXmppConnectionService.getString(R.string.mark_as_read),
1218 markAsReadPendingIntent)
1219 .setSemanticAction(
1220 NotificationCompat.Action.SEMANTIC_ACTION_MARK_AS_READ)
1221 .setShowsUserInterface(false)
1222 .build();
1223 final String replyLabel = mXmppConnectionService.getString(R.string.reply);
1224 final String lastMessageUuid = Iterables.getLast(messages).getUuid();
1225 final NotificationCompat.Action replyAction =
1226 new NotificationCompat.Action.Builder(
1227 R.drawable.ic_send_text_offline,
1228 replyLabel,
1229 createReplyIntent(conversation, lastMessageUuid, false))
1230 .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY)
1231 .setShowsUserInterface(false)
1232 .addRemoteInput(remoteInput)
1233 .build();
1234 final NotificationCompat.Action wearReplyAction =
1235 new NotificationCompat.Action.Builder(
1236 R.drawable.ic_wear_reply,
1237 replyLabel,
1238 createReplyIntent(conversation, lastMessageUuid, true))
1239 .addRemoteInput(remoteInput)
1240 .build();
1241 mBuilder.extend(
1242 new NotificationCompat.WearableExtender().addAction(wearReplyAction));
1243 int addedActionsCount = 1;
1244 mBuilder.addAction(markReadAction);
1245 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
1246 mBuilder.addAction(replyAction);
1247 ++addedActionsCount;
1248 }
1249
1250 if (displaySnoozeAction(messages)) {
1251 String label = mXmppConnectionService.getString(R.string.snooze);
1252 PendingIntent pendingSnoozeIntent = createSnoozeIntent(conversation);
1253 NotificationCompat.Action snoozeAction =
1254 new NotificationCompat.Action.Builder(
1255 R.drawable.ic_notifications_paused_white_24dp,
1256 label,
1257 pendingSnoozeIntent)
1258 .build();
1259 mBuilder.addAction(snoozeAction);
1260 ++addedActionsCount;
1261 }
1262 if (addedActionsCount < 3) {
1263 final Message firstLocationMessage = getFirstLocationMessage(messages);
1264 if (firstLocationMessage != null) {
1265 final PendingIntent pendingShowLocationIntent =
1266 createShowLocationIntent(firstLocationMessage);
1267 if (pendingShowLocationIntent != null) {
1268 final String label =
1269 mXmppConnectionService
1270 .getResources()
1271 .getString(R.string.show_location);
1272 NotificationCompat.Action locationAction =
1273 new NotificationCompat.Action.Builder(
1274 R.drawable.ic_room_white_24dp,
1275 label,
1276 pendingShowLocationIntent)
1277 .build();
1278 mBuilder.addAction(locationAction);
1279 ++addedActionsCount;
1280 }
1281 }
1282 }
1283 if (addedActionsCount < 3) {
1284 Message firstDownloadableMessage = getFirstDownloadableMessage(messages);
1285 if (firstDownloadableMessage != null) {
1286 String label =
1287 mXmppConnectionService
1288 .getResources()
1289 .getString(
1290 R.string.download_x_file,
1291 UIHelper.getFileDescriptionString(
1292 mXmppConnectionService,
1293 firstDownloadableMessage));
1294 PendingIntent pendingDownloadIntent =
1295 createDownloadIntent(firstDownloadableMessage);
1296 NotificationCompat.Action downloadAction =
1297 new NotificationCompat.Action.Builder(
1298 R.drawable.ic_file_download_white_24dp,
1299 label,
1300 pendingDownloadIntent)
1301 .build();
1302 mBuilder.addAction(downloadAction);
1303 ++addedActionsCount;
1304 }
1305 }
1306 }
1307 final ShortcutInfoCompat info;
1308 if (conversation.getMode() == Conversation.MODE_SINGLE) {
1309 final Contact contact = conversation.getContact();
1310 final Uri systemAccount = contact.getSystemAccount();
1311 if (systemAccount != null) {
1312 mBuilder.addPerson(systemAccount.toString());
1313 }
1314 info = mXmppConnectionService.getShortcutService().getShortcutInfoCompat(contact);
1315 } else {
1316 info =
1317 mXmppConnectionService
1318 .getShortcutService()
1319 .getShortcutInfoCompat(conversation.getMucOptions());
1320 }
1321 mBuilder.setWhen(conversation.getLatestMessage().getTimeSent());
1322 mBuilder.setSmallIcon(R.drawable.ic_notification);
1323 mBuilder.setDeleteIntent(createDeleteIntent(conversation));
1324 mBuilder.setContentIntent(createContentIntent(conversation));
1325 mBuilder.setShortcutInfo(info);
1326 if (Build.VERSION.SDK_INT >= 30) {
1327 mXmppConnectionService
1328 .getSystemService(ShortcutManager.class)
1329 .pushDynamicShortcut(info.toShortcutInfo());
1330 }
1331 }
1332 return mBuilder;
1333 }
1334
1335 private void modifyForImage(
1336 final Builder builder, final Message message, final ArrayList<Message> messages) {
1337 try {
1338 final Bitmap bitmap =
1339 mXmppConnectionService
1340 .getFileBackend()
1341 .getThumbnail(message, getPixel(288), false);
1342 final ArrayList<Message> tmp = new ArrayList<>();
1343 for (final Message msg : messages) {
1344 if (msg.getType() == Message.TYPE_TEXT && msg.getTransferable() == null) {
1345 tmp.add(msg);
1346 }
1347 }
1348 final BigPictureStyle bigPictureStyle = new NotificationCompat.BigPictureStyle();
1349 bigPictureStyle.bigPicture(bitmap);
1350 if (tmp.size() > 0) {
1351 CharSequence text = getMergedBodies(tmp);
1352 bigPictureStyle.setSummaryText(text);
1353 builder.setContentText(text);
1354 builder.setTicker(text);
1355 } else {
1356 final String description =
1357 UIHelper.getFileDescriptionString(mXmppConnectionService, message);
1358 builder.setContentText(description);
1359 builder.setTicker(description);
1360 }
1361 builder.setStyle(bigPictureStyle);
1362 } catch (final IOException e) {
1363 modifyForTextOnly(builder, messages);
1364 }
1365 }
1366
1367 private Person getPerson(Message message) {
1368 final Contact contact = message.getContact();
1369 final Person.Builder builder = new Person.Builder();
1370 if (contact != null) {
1371 builder.setName(contact.getDisplayName());
1372 final Uri uri = contact.getSystemAccount();
1373 if (uri != null) {
1374 builder.setUri(uri.toString());
1375 }
1376 } else {
1377 builder.setName(UIHelper.getMessageDisplayName(message));
1378 }
1379 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
1380 builder.setIcon(
1381 IconCompat.createWithBitmap(
1382 mXmppConnectionService
1383 .getAvatarService()
1384 .get(
1385 message,
1386 AvatarService.getSystemUiAvatarSize(
1387 mXmppConnectionService),
1388 false)));
1389 }
1390 return builder.build();
1391 }
1392
1393 private void modifyForTextOnly(final Builder builder, final ArrayList<Message> messages) {
1394 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
1395 final Conversation conversation = (Conversation) messages.get(0).getConversation();
1396 final Person.Builder meBuilder =
1397 new Person.Builder().setName(mXmppConnectionService.getString(R.string.me));
1398 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
1399 meBuilder.setIcon(
1400 IconCompat.createWithBitmap(
1401 mXmppConnectionService
1402 .getAvatarService()
1403 .get(
1404 conversation.getAccount(),
1405 AvatarService.getSystemUiAvatarSize(
1406 mXmppConnectionService))));
1407 }
1408 final Person me = meBuilder.build();
1409 NotificationCompat.MessagingStyle messagingStyle =
1410 new NotificationCompat.MessagingStyle(me);
1411 final boolean multiple = conversation.getMode() == Conversation.MODE_MULTI;
1412 if (multiple) {
1413 messagingStyle.setConversationTitle(conversation.getName());
1414 }
1415 for (Message message : messages) {
1416 final Person sender =
1417 message.getStatus() == Message.STATUS_RECEIVED ? getPerson(message) : null;
1418 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && isImageMessage(message)) {
1419 final Uri dataUri =
1420 FileBackend.getMediaUri(
1421 mXmppConnectionService,
1422 mXmppConnectionService.getFileBackend().getFile(message));
1423 NotificationCompat.MessagingStyle.Message imageMessage =
1424 new NotificationCompat.MessagingStyle.Message(
1425 UIHelper.getMessagePreview(mXmppConnectionService, message)
1426 .first,
1427 message.getTimeSent(),
1428 sender);
1429 if (dataUri != null) {
1430 imageMessage.setData(message.getMimeType(), dataUri);
1431 }
1432 messagingStyle.addMessage(imageMessage);
1433 } else {
1434 messagingStyle.addMessage(
1435 UIHelper.getMessagePreview(mXmppConnectionService, message).first,
1436 message.getTimeSent(),
1437 sender);
1438 }
1439 }
1440 messagingStyle.setGroupConversation(multiple);
1441 builder.setStyle(messagingStyle);
1442 } else {
1443 if (messages.get(0).getConversation().getMode() == Conversation.MODE_SINGLE) {
1444 builder.setStyle(
1445 new NotificationCompat.BigTextStyle().bigText(getMergedBodies(messages)));
1446 final CharSequence preview =
1447 UIHelper.getMessagePreview(
1448 mXmppConnectionService, messages.get(messages.size() - 1))
1449 .first;
1450 builder.setContentText(preview);
1451 builder.setTicker(preview);
1452 builder.setNumber(messages.size());
1453 } else {
1454 final NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle();
1455 SpannableString styledString;
1456 for (Message message : messages) {
1457 final String name = UIHelper.getMessageDisplayName(message);
1458 styledString = new SpannableString(name + ": " + message.getBody());
1459 styledString.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), 0);
1460 style.addLine(styledString);
1461 }
1462 builder.setStyle(style);
1463 int count = messages.size();
1464 if (count == 1) {
1465 final String name = UIHelper.getMessageDisplayName(messages.get(0));
1466 styledString = new SpannableString(name + ": " + messages.get(0).getBody());
1467 styledString.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), 0);
1468 builder.setContentText(styledString);
1469 builder.setTicker(styledString);
1470 } else {
1471 final String text =
1472 mXmppConnectionService
1473 .getResources()
1474 .getQuantityString(R.plurals.x_messages, count, count);
1475 builder.setContentText(text);
1476 builder.setTicker(text);
1477 }
1478 }
1479 }
1480 }
1481
1482 private Message getImage(final Iterable<Message> messages) {
1483 Message image = null;
1484 for (final Message message : messages) {
1485 if (message.getStatus() != Message.STATUS_RECEIVED) {
1486 return null;
1487 }
1488 if (isImageMessage(message)) {
1489 image = message;
1490 }
1491 }
1492 return image;
1493 }
1494
1495 private Message getFirstDownloadableMessage(final Iterable<Message> messages) {
1496 for (final Message message : messages) {
1497 if (message.getTransferable() != null
1498 || (message.getType() == Message.TYPE_TEXT && message.treatAsDownloadable())) {
1499 return message;
1500 }
1501 }
1502 return null;
1503 }
1504
1505 private Message getFirstLocationMessage(final Iterable<Message> messages) {
1506 for (final Message message : messages) {
1507 if (message.isGeoUri()) {
1508 return message;
1509 }
1510 }
1511 return null;
1512 }
1513
1514 private CharSequence getMergedBodies(final ArrayList<Message> messages) {
1515 final StringBuilder text = new StringBuilder();
1516 for (Message message : messages) {
1517 if (text.length() != 0) {
1518 text.append("\n");
1519 }
1520 text.append(UIHelper.getMessagePreview(mXmppConnectionService, message).first);
1521 }
1522 return text.toString();
1523 }
1524
1525 private PendingIntent createShowLocationIntent(final Message message) {
1526 Iterable<Intent> intents =
1527 GeoHelper.createGeoIntentsFromMessage(mXmppConnectionService, message);
1528 for (final Intent intent : intents) {
1529 if (intent.resolveActivity(mXmppConnectionService.getPackageManager()) != null) {
1530 return PendingIntent.getActivity(
1531 mXmppConnectionService,
1532 generateRequestCode(message.getConversation(), 18),
1533 intent,
1534 s()
1535 ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1536 : PendingIntent.FLAG_UPDATE_CURRENT);
1537 }
1538 }
1539 return null;
1540 }
1541
1542 private PendingIntent createContentIntent(
1543 final String conversationUuid, final String downloadMessageUuid) {
1544 final Intent viewConversationIntent =
1545 new Intent(mXmppConnectionService, ConversationsActivity.class);
1546 viewConversationIntent.setAction(ConversationsActivity.ACTION_VIEW_CONVERSATION);
1547 viewConversationIntent.putExtra(ConversationsActivity.EXTRA_CONVERSATION, conversationUuid);
1548 if (downloadMessageUuid != null) {
1549 viewConversationIntent.putExtra(
1550 ConversationsActivity.EXTRA_DOWNLOAD_UUID, downloadMessageUuid);
1551 return PendingIntent.getActivity(
1552 mXmppConnectionService,
1553 generateRequestCode(conversationUuid, 8),
1554 viewConversationIntent,
1555 s()
1556 ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1557 : PendingIntent.FLAG_UPDATE_CURRENT);
1558 } else {
1559 return PendingIntent.getActivity(
1560 mXmppConnectionService,
1561 generateRequestCode(conversationUuid, 10),
1562 viewConversationIntent,
1563 s()
1564 ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1565 : PendingIntent.FLAG_UPDATE_CURRENT);
1566 }
1567 }
1568
1569 private int generateRequestCode(String uuid, int actionId) {
1570 return (actionId * NOTIFICATION_ID_MULTIPLIER)
1571 + (uuid.hashCode() % NOTIFICATION_ID_MULTIPLIER);
1572 }
1573
1574 private int generateRequestCode(Conversational conversation, int actionId) {
1575 return generateRequestCode(conversation.getUuid(), actionId);
1576 }
1577
1578 private PendingIntent createDownloadIntent(final Message message) {
1579 return createContentIntent(message.getConversationUuid(), message.getUuid());
1580 }
1581
1582 private PendingIntent createContentIntent(final Conversational conversation) {
1583 return createContentIntent(conversation.getUuid(), null);
1584 }
1585
1586 private PendingIntent createDeleteIntent(final Conversation conversation) {
1587 final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
1588 intent.setAction(XmppConnectionService.ACTION_CLEAR_MESSAGE_NOTIFICATION);
1589 if (conversation != null) {
1590 intent.putExtra("uuid", conversation.getUuid());
1591 return PendingIntent.getService(
1592 mXmppConnectionService,
1593 generateRequestCode(conversation, 20),
1594 intent,
1595 s()
1596 ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1597 : PendingIntent.FLAG_UPDATE_CURRENT);
1598 }
1599 return PendingIntent.getService(
1600 mXmppConnectionService,
1601 0,
1602 intent,
1603 s()
1604 ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1605 : PendingIntent.FLAG_UPDATE_CURRENT);
1606 }
1607
1608 private PendingIntent createMissedCallsDeleteIntent(final Conversational conversation) {
1609 final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
1610 intent.setAction(XmppConnectionService.ACTION_CLEAR_MISSED_CALL_NOTIFICATION);
1611 if (conversation != null) {
1612 intent.putExtra("uuid", conversation.getUuid());
1613 return PendingIntent.getService(
1614 mXmppConnectionService,
1615 generateRequestCode(conversation, 21),
1616 intent,
1617 s()
1618 ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1619 : PendingIntent.FLAG_UPDATE_CURRENT);
1620 }
1621 return PendingIntent.getService(
1622 mXmppConnectionService,
1623 1,
1624 intent,
1625 s()
1626 ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1627 : PendingIntent.FLAG_UPDATE_CURRENT);
1628 }
1629
1630 private PendingIntent createReplyIntent(
1631 final Conversation conversation,
1632 final String lastMessageUuid,
1633 final boolean dismissAfterReply) {
1634 final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
1635 intent.setAction(XmppConnectionService.ACTION_REPLY_TO_CONVERSATION);
1636 intent.putExtra("uuid", conversation.getUuid());
1637 intent.putExtra("dismiss_notification", dismissAfterReply);
1638 intent.putExtra("last_message_uuid", lastMessageUuid);
1639 final int id = generateRequestCode(conversation, dismissAfterReply ? 12 : 14);
1640 return PendingIntent.getService(
1641 mXmppConnectionService,
1642 id,
1643 intent,
1644 s()
1645 ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1646 : PendingIntent.FLAG_UPDATE_CURRENT);
1647 }
1648
1649 private PendingIntent createReadPendingIntent(Conversation conversation) {
1650 final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
1651 intent.setAction(XmppConnectionService.ACTION_MARK_AS_READ);
1652 intent.putExtra("uuid", conversation.getUuid());
1653 intent.setPackage(mXmppConnectionService.getPackageName());
1654 return PendingIntent.getService(
1655 mXmppConnectionService,
1656 generateRequestCode(conversation, 16),
1657 intent,
1658 s()
1659 ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1660 : PendingIntent.FLAG_UPDATE_CURRENT);
1661 }
1662
1663 private PendingIntent createCallAction(String sessionId, final String action, int requestCode) {
1664 final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
1665 intent.setAction(action);
1666 intent.setPackage(mXmppConnectionService.getPackageName());
1667 intent.putExtra(RtpSessionActivity.EXTRA_SESSION_ID, sessionId);
1668 return PendingIntent.getService(
1669 mXmppConnectionService,
1670 requestCode,
1671 intent,
1672 s()
1673 ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1674 : PendingIntent.FLAG_UPDATE_CURRENT);
1675 }
1676
1677 private PendingIntent createSnoozeIntent(Conversation conversation) {
1678 final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
1679 intent.setAction(XmppConnectionService.ACTION_SNOOZE);
1680 intent.putExtra("uuid", conversation.getUuid());
1681 intent.setPackage(mXmppConnectionService.getPackageName());
1682 return PendingIntent.getService(
1683 mXmppConnectionService,
1684 generateRequestCode(conversation, 22),
1685 intent,
1686 s()
1687 ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1688 : PendingIntent.FLAG_UPDATE_CURRENT);
1689 }
1690
1691 private PendingIntent createTryAgainIntent() {
1692 final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
1693 intent.setAction(XmppConnectionService.ACTION_TRY_AGAIN);
1694 return PendingIntent.getService(
1695 mXmppConnectionService,
1696 45,
1697 intent,
1698 s()
1699 ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1700 : PendingIntent.FLAG_UPDATE_CURRENT);
1701 }
1702
1703 private PendingIntent createDismissErrorIntent() {
1704 final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
1705 intent.setAction(XmppConnectionService.ACTION_DISMISS_ERROR_NOTIFICATIONS);
1706 return PendingIntent.getService(
1707 mXmppConnectionService,
1708 69,
1709 intent,
1710 s()
1711 ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1712 : PendingIntent.FLAG_UPDATE_CURRENT);
1713 }
1714
1715 private boolean wasHighlightedOrPrivate(final Message message) {
1716 if (message.getConversation() instanceof Conversation) {
1717 Conversation conversation = (Conversation) message.getConversation();
1718 final String nick = conversation.getMucOptions().getActualNick();
1719 final Pattern highlight = generateNickHighlightPattern(nick);
1720 if (message.getBody() == null || nick == null) {
1721 return false;
1722 }
1723 final Matcher m = highlight.matcher(message.getBody());
1724 return (m.find() || message.isPrivateMessage());
1725 } else {
1726 return false;
1727 }
1728 }
1729
1730 public void setOpenConversation(final Conversation conversation) {
1731 this.mOpenConversation = conversation;
1732 }
1733
1734 public void setIsInForeground(final boolean foreground) {
1735 this.mIsInForeground = foreground;
1736 }
1737
1738 private int getPixel(final int dp) {
1739 final DisplayMetrics metrics = mXmppConnectionService.getResources().getDisplayMetrics();
1740 return ((int) (dp * metrics.density));
1741 }
1742
1743 private void markLastNotification() {
1744 this.mLastNotification = SystemClock.elapsedRealtime();
1745 }
1746
1747 private boolean inMiniGracePeriod(final Account account) {
1748 final int miniGrace =
1749 account.getStatus() == Account.State.ONLINE
1750 ? Config.MINI_GRACE_PERIOD
1751 : Config.MINI_GRACE_PERIOD * 2;
1752 return SystemClock.elapsedRealtime() < (this.mLastNotification + miniGrace);
1753 }
1754
1755 Notification createForegroundNotification() {
1756 final Notification.Builder mBuilder = new Notification.Builder(mXmppConnectionService);
1757 mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.app_name));
1758 final List<Account> accounts = mXmppConnectionService.getAccounts();
1759 int enabled = 0;
1760 int connected = 0;
1761 if (accounts != null) {
1762 for (Account account : accounts) {
1763 if (account.isOnlineAndConnected()) {
1764 connected++;
1765 enabled++;
1766 } else if (account.isEnabled()) {
1767 enabled++;
1768 }
1769 }
1770 }
1771 mBuilder.setContentText(
1772 mXmppConnectionService.getString(R.string.connected_accounts, connected, enabled));
1773 final PendingIntent openIntent = createOpenConversationsIntent();
1774 if (openIntent != null) {
1775 mBuilder.setContentIntent(openIntent);
1776 }
1777 mBuilder.setWhen(0)
1778 .setPriority(Notification.PRIORITY_MIN)
1779 .setSmallIcon(
1780 connected > 0
1781 ? R.drawable.ic_link_white_24dp
1782 : R.drawable.ic_link_off_white_24dp)
1783 .setLocalOnly(true);
1784
1785 if (Compatibility.runsTwentySix()) {
1786 mBuilder.setChannelId("foreground");
1787 }
1788
1789 return mBuilder.build();
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 createTryAgainIntent());
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(createDismissErrorIntent());
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}