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 clearMissedCalls() {
777 synchronized (mMissedCalls) {
778 for (final Conversational conversation : mMissedCalls.keySet()) {
779 cancel(conversation.getUuid(), MISSED_CALL_NOTIFICATION_ID);
780 }
781 mMissedCalls.clear();
782 updateMissedCallNotifications(null);
783 }
784 }
785
786 public void clearMissedCalls(final Conversation conversation) {
787 synchronized (mMissedCalls) {
788 if (mMissedCalls.remove(conversation) != null) {
789 cancel(conversation.getUuid(), MISSED_CALL_NOTIFICATION_ID);
790 updateMissedCallNotifications(null);
791 }
792 }
793 }
794
795 private void markAsReadIfHasDirectReply(final Conversation conversation) {
796 markAsReadIfHasDirectReply(notifications.get(conversation.getUuid()));
797 }
798
799 private void markAsReadIfHasDirectReply(final ArrayList<Message> messages) {
800 if (messages != null && messages.size() > 0) {
801 Message last = messages.get(messages.size() - 1);
802 if (last.getStatus() != Message.STATUS_RECEIVED) {
803 if (mXmppConnectionService.markRead((Conversation) last.getConversation(), false)) {
804 mXmppConnectionService.updateConversationUi();
805 }
806 }
807 }
808 }
809
810 private void setNotificationColor(final Builder mBuilder) {
811 mBuilder.setColor(ContextCompat.getColor(mXmppConnectionService, R.color.green600));
812 }
813
814 public void updateNotification() {
815 synchronized (notifications) {
816 updateNotification(false);
817 }
818 }
819
820 private void updateNotification(final boolean notify) {
821 updateNotification(notify, null, false);
822 }
823
824 private void updateNotification(final boolean notify, final List<String> conversations) {
825 updateNotification(notify, conversations, false);
826 }
827
828 private void updateNotification(
829 final boolean notify, final List<String> conversations, final boolean summaryOnly) {
830 final SharedPreferences preferences =
831 PreferenceManager.getDefaultSharedPreferences(mXmppConnectionService);
832
833 final boolean quiteHours = isQuietHours();
834
835 final boolean notifyOnlyOneChild =
836 notify
837 && conversations != null
838 && conversations.size()
839 == 1; // if this check is changed to > 0 catchup messages will
840 // create one notification per conversation
841
842 if (notifications.size() == 0) {
843 cancel(NOTIFICATION_ID);
844 } else {
845 if (notify) {
846 this.markLastNotification();
847 }
848 final Builder mBuilder;
849 if (notifications.size() == 1 && Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
850 mBuilder =
851 buildSingleConversations(
852 notifications.values().iterator().next(), notify, quiteHours);
853 modifyForSoundVibrationAndLight(mBuilder, notify, quiteHours, preferences);
854 notify(NOTIFICATION_ID, mBuilder.build());
855 } else {
856 mBuilder = buildMultipleConversation(notify, quiteHours);
857 if (notifyOnlyOneChild) {
858 mBuilder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN);
859 }
860 modifyForSoundVibrationAndLight(mBuilder, notify, quiteHours, preferences);
861 if (!summaryOnly) {
862 for (Map.Entry<String, ArrayList<Message>> entry : notifications.entrySet()) {
863 String uuid = entry.getKey();
864 final boolean notifyThis =
865 notifyOnlyOneChild ? conversations.contains(uuid) : notify;
866 Builder singleBuilder =
867 buildSingleConversations(entry.getValue(), notifyThis, quiteHours);
868 if (!notifyOnlyOneChild) {
869 singleBuilder.setGroupAlertBehavior(
870 NotificationCompat.GROUP_ALERT_SUMMARY);
871 }
872 modifyForSoundVibrationAndLight(
873 singleBuilder, notifyThis, quiteHours, preferences);
874 singleBuilder.setGroup(MESSAGES_GROUP);
875 setNotificationColor(singleBuilder);
876 notify(entry.getKey(), NOTIFICATION_ID, singleBuilder.build());
877 }
878 }
879 notify(NOTIFICATION_ID, mBuilder.build());
880 }
881 }
882 }
883
884 private void updateMissedCallNotifications(final Set<Conversational> update) {
885 if (mMissedCalls.isEmpty()) {
886 cancel(MISSED_CALL_NOTIFICATION_ID);
887 return;
888 }
889 if (mMissedCalls.size() == 1 && Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
890 final Conversational conversation = mMissedCalls.keySet().iterator().next();
891 final MissedCallsInfo info = mMissedCalls.values().iterator().next();
892 final Notification notification = missedCall(conversation, info);
893 notify(MISSED_CALL_NOTIFICATION_ID, notification);
894 } else {
895 final Notification summary = missedCallsSummary();
896 notify(MISSED_CALL_NOTIFICATION_ID, summary);
897 if (update != null) {
898 for (final Conversational conversation : update) {
899 final MissedCallsInfo info = mMissedCalls.get(conversation);
900 if (info != null) {
901 final Notification notification = missedCall(conversation, info);
902 notify(conversation.getUuid(), MISSED_CALL_NOTIFICATION_ID, notification);
903 }
904 }
905 }
906 }
907 }
908
909 private void modifyForSoundVibrationAndLight(
910 Builder mBuilder, boolean notify, boolean quietHours, SharedPreferences preferences) {
911 final Resources resources = mXmppConnectionService.getResources();
912 final String ringtone =
913 preferences.getString(
914 "notification_ringtone",
915 resources.getString(R.string.notification_ringtone));
916 final boolean vibrate =
917 preferences.getBoolean(
918 "vibrate_on_notification",
919 resources.getBoolean(R.bool.vibrate_on_notification));
920 final boolean led = preferences.getBoolean("led", resources.getBoolean(R.bool.led));
921 final boolean headsup =
922 preferences.getBoolean(
923 "notification_headsup", resources.getBoolean(R.bool.headsup_notifications));
924 if (notify && !quietHours) {
925 if (vibrate) {
926 final int dat = 70;
927 final long[] pattern = {0, 3 * dat, dat, dat};
928 mBuilder.setVibrate(pattern);
929 } else {
930 mBuilder.setVibrate(new long[] {0});
931 }
932 Uri uri = Uri.parse(ringtone);
933 try {
934 mBuilder.setSound(fixRingtoneUri(uri));
935 } catch (SecurityException e) {
936 Log.d(Config.LOGTAG, "unable to use custom notification sound " + uri.toString());
937 }
938 } else {
939 mBuilder.setLocalOnly(true);
940 }
941 mBuilder.setCategory(Notification.CATEGORY_MESSAGE);
942 mBuilder.setPriority(
943 notify
944 ? (headsup
945 ? NotificationCompat.PRIORITY_HIGH
946 : NotificationCompat.PRIORITY_DEFAULT)
947 : NotificationCompat.PRIORITY_LOW);
948 setNotificationColor(mBuilder);
949 mBuilder.setDefaults(0);
950 if (led) {
951 mBuilder.setLights(LED_COLOR, 2000, 3000);
952 }
953 }
954
955 private void modifyIncomingCall(final Builder mBuilder) {
956 mBuilder.setPriority(NotificationCompat.PRIORITY_HIGH);
957 setNotificationColor(mBuilder);
958 mBuilder.setLights(LED_COLOR, 2000, 3000);
959 }
960
961 private Uri fixRingtoneUri(Uri uri) {
962 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && "file".equals(uri.getScheme())) {
963 return FileBackend.getUriForFile(mXmppConnectionService, new File(uri.getPath()));
964 } else {
965 return uri;
966 }
967 }
968
969 private Notification missedCallsSummary() {
970 final Builder publicBuilder = buildMissedCallsSummary(true);
971 final Builder builder = buildMissedCallsSummary(false);
972 builder.setPublicVersion(publicBuilder.build());
973 return builder.build();
974 }
975
976 private Builder buildMissedCallsSummary(boolean publicVersion) {
977 final Builder builder =
978 new NotificationCompat.Builder(mXmppConnectionService, "missed_calls");
979 int totalCalls = 0;
980 final List<String> names = new ArrayList<>();
981 long lastTime = 0;
982 for (final Map.Entry<Conversational, MissedCallsInfo> entry : mMissedCalls.entrySet()) {
983 final Conversational conversation = entry.getKey();
984 final MissedCallsInfo missedCallsInfo = entry.getValue();
985 names.add(conversation.getContact().getDisplayName());
986 totalCalls += missedCallsInfo.getNumberOfCalls();
987 lastTime = Math.max(lastTime, missedCallsInfo.getLastTime());
988 }
989 final String title =
990 (totalCalls == 1)
991 ? mXmppConnectionService.getString(R.string.missed_call)
992 : (mMissedCalls.size() == 1)
993 ? mXmppConnectionService
994 .getResources()
995 .getQuantityString(
996 R.plurals.n_missed_calls, totalCalls, totalCalls)
997 : mXmppConnectionService
998 .getResources()
999 .getQuantityString(
1000 R.plurals.n_missed_calls_from_m_contacts,
1001 mMissedCalls.size(),
1002 totalCalls,
1003 mMissedCalls.size());
1004 builder.setContentTitle(title);
1005 builder.setTicker(title);
1006 if (!publicVersion) {
1007 builder.setContentText(Joiner.on(", ").join(names));
1008 }
1009 builder.setSmallIcon(R.drawable.ic_call_missed_white_24db);
1010 builder.setGroupSummary(true);
1011 builder.setGroup(MISSED_CALLS_GROUP);
1012 builder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN);
1013 builder.setCategory(NotificationCompat.CATEGORY_CALL);
1014 builder.setWhen(lastTime);
1015 if (!mMissedCalls.isEmpty()) {
1016 final Conversational firstConversation = mMissedCalls.keySet().iterator().next();
1017 builder.setContentIntent(createContentIntent(firstConversation));
1018 }
1019 builder.setDeleteIntent(createMissedCallsDeleteIntent(null));
1020 modifyMissedCall(builder);
1021 return builder;
1022 }
1023
1024 private Notification missedCall(final Conversational conversation, final MissedCallsInfo info) {
1025 final Builder publicBuilder = buildMissedCall(conversation, info, true);
1026 final Builder builder = buildMissedCall(conversation, info, false);
1027 builder.setPublicVersion(publicBuilder.build());
1028 return builder.build();
1029 }
1030
1031 private Builder buildMissedCall(
1032 final Conversational conversation, final MissedCallsInfo info, boolean publicVersion) {
1033 final Builder builder =
1034 new NotificationCompat.Builder(mXmppConnectionService, "missed_calls");
1035 final String title =
1036 (info.getNumberOfCalls() == 1)
1037 ? mXmppConnectionService.getString(R.string.missed_call)
1038 : mXmppConnectionService
1039 .getResources()
1040 .getQuantityString(
1041 R.plurals.n_missed_calls,
1042 info.getNumberOfCalls(),
1043 info.getNumberOfCalls());
1044 builder.setContentTitle(title);
1045 final String name = conversation.getContact().getDisplayName();
1046 if (publicVersion) {
1047 builder.setTicker(title);
1048 } else {
1049 builder.setTicker(
1050 mXmppConnectionService
1051 .getResources()
1052 .getQuantityString(
1053 R.plurals.n_missed_calls_from_x,
1054 info.getNumberOfCalls(),
1055 info.getNumberOfCalls(),
1056 name));
1057 builder.setContentText(name);
1058 }
1059 builder.setSmallIcon(R.drawable.ic_call_missed_white_24db);
1060 builder.setGroup(MISSED_CALLS_GROUP);
1061 builder.setCategory(NotificationCompat.CATEGORY_CALL);
1062 builder.setWhen(info.getLastTime());
1063 builder.setContentIntent(createContentIntent(conversation));
1064 builder.setDeleteIntent(createMissedCallsDeleteIntent(conversation));
1065 if (!publicVersion && conversation instanceof Conversation) {
1066 builder.setLargeIcon(
1067 mXmppConnectionService
1068 .getAvatarService()
1069 .get(
1070 (Conversation) conversation,
1071 AvatarService.getSystemUiAvatarSize(mXmppConnectionService)));
1072 }
1073 modifyMissedCall(builder);
1074 return builder;
1075 }
1076
1077 private void modifyMissedCall(final Builder builder) {
1078 final SharedPreferences preferences =
1079 PreferenceManager.getDefaultSharedPreferences(mXmppConnectionService);
1080 final Resources resources = mXmppConnectionService.getResources();
1081 final boolean led = preferences.getBoolean("led", resources.getBoolean(R.bool.led));
1082 if (led) {
1083 builder.setLights(LED_COLOR, 2000, 3000);
1084 }
1085 builder.setPriority(NotificationCompat.PRIORITY_HIGH);
1086 builder.setSound(null);
1087 setNotificationColor(builder);
1088 }
1089
1090 private Builder buildMultipleConversation(final boolean notify, final boolean quietHours) {
1091 final Builder mBuilder =
1092 new NotificationCompat.Builder(
1093 mXmppConnectionService,
1094 quietHours ? "quiet_hours" : (notify ? "messages" : "silent_messages"));
1095 final NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle();
1096 style.setBigContentTitle(
1097 mXmppConnectionService
1098 .getResources()
1099 .getQuantityString(
1100 R.plurals.x_unread_conversations,
1101 notifications.size(),
1102 notifications.size()));
1103 final List<String> names = new ArrayList<>();
1104 Conversation conversation = null;
1105 for (final ArrayList<Message> messages : notifications.values()) {
1106 if (messages.isEmpty()) {
1107 continue;
1108 }
1109 conversation = (Conversation) messages.get(0).getConversation();
1110 final String name = conversation.getName().toString();
1111 SpannableString styledString;
1112 if (Config.HIDE_MESSAGE_TEXT_IN_NOTIFICATION) {
1113 int count = messages.size();
1114 styledString =
1115 new SpannableString(
1116 name
1117 + ": "
1118 + mXmppConnectionService
1119 .getResources()
1120 .getQuantityString(
1121 R.plurals.x_messages, count, count));
1122 styledString.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), 0);
1123 style.addLine(styledString);
1124 } else {
1125 styledString =
1126 new SpannableString(
1127 name
1128 + ": "
1129 + UIHelper.getMessagePreview(
1130 mXmppConnectionService, messages.get(0))
1131 .first);
1132 styledString.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), 0);
1133 style.addLine(styledString);
1134 }
1135 names.add(name);
1136 }
1137 final String contentTitle =
1138 mXmppConnectionService
1139 .getResources()
1140 .getQuantityString(
1141 R.plurals.x_unread_conversations,
1142 notifications.size(),
1143 notifications.size());
1144 mBuilder.setContentTitle(contentTitle);
1145 mBuilder.setTicker(contentTitle);
1146 mBuilder.setContentText(Joiner.on(", ").join(names));
1147 mBuilder.setStyle(style);
1148 if (conversation != null) {
1149 mBuilder.setContentIntent(createContentIntent(conversation));
1150 }
1151 mBuilder.setGroupSummary(true);
1152 mBuilder.setGroup(MESSAGES_GROUP);
1153 mBuilder.setDeleteIntent(createDeleteIntent(null));
1154 mBuilder.setSmallIcon(R.drawable.ic_notification);
1155 return mBuilder;
1156 }
1157
1158 private Builder buildSingleConversations(
1159 final ArrayList<Message> messages, final boolean notify, final boolean quietHours) {
1160 final Builder mBuilder =
1161 new NotificationCompat.Builder(
1162 mXmppConnectionService,
1163 quietHours ? "quiet_hours" : (notify ? "messages" : "silent_messages"));
1164 if (messages.size() >= 1) {
1165 final Conversation conversation = (Conversation) messages.get(0).getConversation();
1166 mBuilder.setLargeIcon(
1167 mXmppConnectionService
1168 .getAvatarService()
1169 .get(
1170 conversation,
1171 AvatarService.getSystemUiAvatarSize(mXmppConnectionService)));
1172 mBuilder.setContentTitle(conversation.getName());
1173 if (Config.HIDE_MESSAGE_TEXT_IN_NOTIFICATION) {
1174 int count = messages.size();
1175 mBuilder.setContentText(
1176 mXmppConnectionService
1177 .getResources()
1178 .getQuantityString(R.plurals.x_messages, count, count));
1179 } else {
1180 Message message;
1181 // TODO starting with Android 9 we might want to put images in MessageStyle
1182 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P
1183 && (message = getImage(messages)) != null) {
1184 modifyForImage(mBuilder, message, messages);
1185 } else {
1186 modifyForTextOnly(mBuilder, messages);
1187 }
1188 RemoteInput remoteInput =
1189 new RemoteInput.Builder("text_reply")
1190 .setLabel(
1191 UIHelper.getMessageHint(
1192 mXmppConnectionService, conversation))
1193 .build();
1194 PendingIntent markAsReadPendingIntent = createReadPendingIntent(conversation);
1195 NotificationCompat.Action markReadAction =
1196 new NotificationCompat.Action.Builder(
1197 R.drawable.ic_drafts_white_24dp,
1198 mXmppConnectionService.getString(R.string.mark_as_read),
1199 markAsReadPendingIntent)
1200 .setSemanticAction(
1201 NotificationCompat.Action.SEMANTIC_ACTION_MARK_AS_READ)
1202 .setShowsUserInterface(false)
1203 .build();
1204 final String replyLabel = mXmppConnectionService.getString(R.string.reply);
1205 final String lastMessageUuid = Iterables.getLast(messages).getUuid();
1206 final NotificationCompat.Action replyAction =
1207 new NotificationCompat.Action.Builder(
1208 R.drawable.ic_send_text_offline,
1209 replyLabel,
1210 createReplyIntent(conversation, lastMessageUuid, false))
1211 .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY)
1212 .setShowsUserInterface(false)
1213 .addRemoteInput(remoteInput)
1214 .build();
1215 final NotificationCompat.Action wearReplyAction =
1216 new NotificationCompat.Action.Builder(
1217 R.drawable.ic_wear_reply,
1218 replyLabel,
1219 createReplyIntent(conversation, lastMessageUuid, true))
1220 .addRemoteInput(remoteInput)
1221 .build();
1222 mBuilder.extend(
1223 new NotificationCompat.WearableExtender().addAction(wearReplyAction));
1224 int addedActionsCount = 1;
1225 mBuilder.addAction(markReadAction);
1226 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
1227 mBuilder.addAction(replyAction);
1228 ++addedActionsCount;
1229 }
1230
1231 if (displaySnoozeAction(messages)) {
1232 String label = mXmppConnectionService.getString(R.string.snooze);
1233 PendingIntent pendingSnoozeIntent = createSnoozeIntent(conversation);
1234 NotificationCompat.Action snoozeAction =
1235 new NotificationCompat.Action.Builder(
1236 R.drawable.ic_notifications_paused_white_24dp,
1237 label,
1238 pendingSnoozeIntent)
1239 .build();
1240 mBuilder.addAction(snoozeAction);
1241 ++addedActionsCount;
1242 }
1243 if (addedActionsCount < 3) {
1244 final Message firstLocationMessage = getFirstLocationMessage(messages);
1245 if (firstLocationMessage != null) {
1246 final PendingIntent pendingShowLocationIntent =
1247 createShowLocationIntent(firstLocationMessage);
1248 if (pendingShowLocationIntent != null) {
1249 final String label =
1250 mXmppConnectionService
1251 .getResources()
1252 .getString(R.string.show_location);
1253 NotificationCompat.Action locationAction =
1254 new NotificationCompat.Action.Builder(
1255 R.drawable.ic_room_white_24dp,
1256 label,
1257 pendingShowLocationIntent)
1258 .build();
1259 mBuilder.addAction(locationAction);
1260 ++addedActionsCount;
1261 }
1262 }
1263 }
1264 if (addedActionsCount < 3) {
1265 Message firstDownloadableMessage = getFirstDownloadableMessage(messages);
1266 if (firstDownloadableMessage != null) {
1267 String label =
1268 mXmppConnectionService
1269 .getResources()
1270 .getString(
1271 R.string.download_x_file,
1272 UIHelper.getFileDescriptionString(
1273 mXmppConnectionService,
1274 firstDownloadableMessage));
1275 PendingIntent pendingDownloadIntent =
1276 createDownloadIntent(firstDownloadableMessage);
1277 NotificationCompat.Action downloadAction =
1278 new NotificationCompat.Action.Builder(
1279 R.drawable.ic_file_download_white_24dp,
1280 label,
1281 pendingDownloadIntent)
1282 .build();
1283 mBuilder.addAction(downloadAction);
1284 ++addedActionsCount;
1285 }
1286 }
1287 }
1288 final ShortcutInfoCompat info;
1289 if (conversation.getMode() == Conversation.MODE_SINGLE) {
1290 final Contact contact = conversation.getContact();
1291 final Uri systemAccount = contact.getSystemAccount();
1292 if (systemAccount != null) {
1293 mBuilder.addPerson(systemAccount.toString());
1294 }
1295 info = mXmppConnectionService.getShortcutService().getShortcutInfoCompat(contact);
1296 } else {
1297 info =
1298 mXmppConnectionService
1299 .getShortcutService()
1300 .getShortcutInfoCompat(conversation.getMucOptions());
1301 }
1302 mBuilder.setWhen(conversation.getLatestMessage().getTimeSent());
1303 mBuilder.setSmallIcon(R.drawable.ic_notification);
1304 mBuilder.setDeleteIntent(createDeleteIntent(conversation));
1305 mBuilder.setContentIntent(createContentIntent(conversation));
1306 mBuilder.setShortcutInfo(info);
1307 if (Build.VERSION.SDK_INT >= 30) {
1308 mXmppConnectionService
1309 .getSystemService(ShortcutManager.class)
1310 .pushDynamicShortcut(info.toShortcutInfo());
1311 }
1312 }
1313 return mBuilder;
1314 }
1315
1316 private void modifyForImage(
1317 final Builder builder, final Message message, final ArrayList<Message> messages) {
1318 try {
1319 final Bitmap bitmap =
1320 mXmppConnectionService
1321 .getFileBackend()
1322 .getThumbnail(message, getPixel(288), false);
1323 final ArrayList<Message> tmp = new ArrayList<>();
1324 for (final Message msg : messages) {
1325 if (msg.getType() == Message.TYPE_TEXT && msg.getTransferable() == null) {
1326 tmp.add(msg);
1327 }
1328 }
1329 final BigPictureStyle bigPictureStyle = new NotificationCompat.BigPictureStyle();
1330 bigPictureStyle.bigPicture(bitmap);
1331 if (tmp.size() > 0) {
1332 CharSequence text = getMergedBodies(tmp);
1333 bigPictureStyle.setSummaryText(text);
1334 builder.setContentText(text);
1335 builder.setTicker(text);
1336 } else {
1337 final String description =
1338 UIHelper.getFileDescriptionString(mXmppConnectionService, message);
1339 builder.setContentText(description);
1340 builder.setTicker(description);
1341 }
1342 builder.setStyle(bigPictureStyle);
1343 } catch (final IOException e) {
1344 modifyForTextOnly(builder, messages);
1345 }
1346 }
1347
1348 private Person getPerson(Message message) {
1349 final Contact contact = message.getContact();
1350 final Person.Builder builder = new Person.Builder();
1351 if (contact != null) {
1352 builder.setName(contact.getDisplayName());
1353 final Uri uri = contact.getSystemAccount();
1354 if (uri != null) {
1355 builder.setUri(uri.toString());
1356 }
1357 } else {
1358 builder.setName(UIHelper.getMessageDisplayName(message));
1359 }
1360 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
1361 builder.setIcon(
1362 IconCompat.createWithBitmap(
1363 mXmppConnectionService
1364 .getAvatarService()
1365 .get(
1366 message,
1367 AvatarService.getSystemUiAvatarSize(
1368 mXmppConnectionService),
1369 false)));
1370 }
1371 return builder.build();
1372 }
1373
1374 private void modifyForTextOnly(final Builder builder, final ArrayList<Message> messages) {
1375 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
1376 final Conversation conversation = (Conversation) messages.get(0).getConversation();
1377 final Person.Builder meBuilder =
1378 new Person.Builder().setName(mXmppConnectionService.getString(R.string.me));
1379 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
1380 meBuilder.setIcon(
1381 IconCompat.createWithBitmap(
1382 mXmppConnectionService
1383 .getAvatarService()
1384 .get(
1385 conversation.getAccount(),
1386 AvatarService.getSystemUiAvatarSize(
1387 mXmppConnectionService))));
1388 }
1389 final Person me = meBuilder.build();
1390 NotificationCompat.MessagingStyle messagingStyle =
1391 new NotificationCompat.MessagingStyle(me);
1392 final boolean multiple = conversation.getMode() == Conversation.MODE_MULTI;
1393 if (multiple) {
1394 messagingStyle.setConversationTitle(conversation.getName());
1395 }
1396 for (Message message : messages) {
1397 final Person sender =
1398 message.getStatus() == Message.STATUS_RECEIVED ? getPerson(message) : null;
1399 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && isImageMessage(message)) {
1400 final Uri dataUri =
1401 FileBackend.getMediaUri(
1402 mXmppConnectionService,
1403 mXmppConnectionService.getFileBackend().getFile(message));
1404 NotificationCompat.MessagingStyle.Message imageMessage =
1405 new NotificationCompat.MessagingStyle.Message(
1406 UIHelper.getMessagePreview(mXmppConnectionService, message)
1407 .first,
1408 message.getTimeSent(),
1409 sender);
1410 if (dataUri != null) {
1411 imageMessage.setData(message.getMimeType(), dataUri);
1412 }
1413 messagingStyle.addMessage(imageMessage);
1414 } else {
1415 messagingStyle.addMessage(
1416 UIHelper.getMessagePreview(mXmppConnectionService, message).first,
1417 message.getTimeSent(),
1418 sender);
1419 }
1420 }
1421 messagingStyle.setGroupConversation(multiple);
1422 builder.setStyle(messagingStyle);
1423 } else {
1424 if (messages.get(0).getConversation().getMode() == Conversation.MODE_SINGLE) {
1425 builder.setStyle(
1426 new NotificationCompat.BigTextStyle().bigText(getMergedBodies(messages)));
1427 final CharSequence preview =
1428 UIHelper.getMessagePreview(
1429 mXmppConnectionService, messages.get(messages.size() - 1))
1430 .first;
1431 builder.setContentText(preview);
1432 builder.setTicker(preview);
1433 builder.setNumber(messages.size());
1434 } else {
1435 final NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle();
1436 SpannableString styledString;
1437 for (Message message : messages) {
1438 final String name = UIHelper.getMessageDisplayName(message);
1439 styledString = new SpannableString(name + ": " + message.getBody());
1440 styledString.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), 0);
1441 style.addLine(styledString);
1442 }
1443 builder.setStyle(style);
1444 int count = messages.size();
1445 if (count == 1) {
1446 final String name = UIHelper.getMessageDisplayName(messages.get(0));
1447 styledString = new SpannableString(name + ": " + messages.get(0).getBody());
1448 styledString.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), 0);
1449 builder.setContentText(styledString);
1450 builder.setTicker(styledString);
1451 } else {
1452 final String text =
1453 mXmppConnectionService
1454 .getResources()
1455 .getQuantityString(R.plurals.x_messages, count, count);
1456 builder.setContentText(text);
1457 builder.setTicker(text);
1458 }
1459 }
1460 }
1461 }
1462
1463 private Message getImage(final Iterable<Message> messages) {
1464 Message image = null;
1465 for (final Message message : messages) {
1466 if (message.getStatus() != Message.STATUS_RECEIVED) {
1467 return null;
1468 }
1469 if (isImageMessage(message)) {
1470 image = message;
1471 }
1472 }
1473 return image;
1474 }
1475
1476 private Message getFirstDownloadableMessage(final Iterable<Message> messages) {
1477 for (final Message message : messages) {
1478 if (message.getTransferable() != null
1479 || (message.getType() == Message.TYPE_TEXT && message.treatAsDownloadable())) {
1480 return message;
1481 }
1482 }
1483 return null;
1484 }
1485
1486 private Message getFirstLocationMessage(final Iterable<Message> messages) {
1487 for (final Message message : messages) {
1488 if (message.isGeoUri()) {
1489 return message;
1490 }
1491 }
1492 return null;
1493 }
1494
1495 private CharSequence getMergedBodies(final ArrayList<Message> messages) {
1496 final StringBuilder text = new StringBuilder();
1497 for (Message message : messages) {
1498 if (text.length() != 0) {
1499 text.append("\n");
1500 }
1501 text.append(UIHelper.getMessagePreview(mXmppConnectionService, message).first);
1502 }
1503 return text.toString();
1504 }
1505
1506 private PendingIntent createShowLocationIntent(final Message message) {
1507 Iterable<Intent> intents =
1508 GeoHelper.createGeoIntentsFromMessage(mXmppConnectionService, message);
1509 for (final Intent intent : intents) {
1510 if (intent.resolveActivity(mXmppConnectionService.getPackageManager()) != null) {
1511 return PendingIntent.getActivity(
1512 mXmppConnectionService,
1513 generateRequestCode(message.getConversation(), 18),
1514 intent,
1515 s()
1516 ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1517 : PendingIntent.FLAG_UPDATE_CURRENT);
1518 }
1519 }
1520 return null;
1521 }
1522
1523 private PendingIntent createContentIntent(
1524 final String conversationUuid, final String downloadMessageUuid) {
1525 final Intent viewConversationIntent =
1526 new Intent(mXmppConnectionService, ConversationsActivity.class);
1527 viewConversationIntent.setAction(ConversationsActivity.ACTION_VIEW_CONVERSATION);
1528 viewConversationIntent.putExtra(ConversationsActivity.EXTRA_CONVERSATION, conversationUuid);
1529 if (downloadMessageUuid != null) {
1530 viewConversationIntent.putExtra(
1531 ConversationsActivity.EXTRA_DOWNLOAD_UUID, downloadMessageUuid);
1532 return PendingIntent.getActivity(
1533 mXmppConnectionService,
1534 generateRequestCode(conversationUuid, 8),
1535 viewConversationIntent,
1536 s()
1537 ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1538 : PendingIntent.FLAG_UPDATE_CURRENT);
1539 } else {
1540 return PendingIntent.getActivity(
1541 mXmppConnectionService,
1542 generateRequestCode(conversationUuid, 10),
1543 viewConversationIntent,
1544 s()
1545 ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1546 : PendingIntent.FLAG_UPDATE_CURRENT);
1547 }
1548 }
1549
1550 private int generateRequestCode(String uuid, int actionId) {
1551 return (actionId * NOTIFICATION_ID_MULTIPLIER)
1552 + (uuid.hashCode() % NOTIFICATION_ID_MULTIPLIER);
1553 }
1554
1555 private int generateRequestCode(Conversational conversation, int actionId) {
1556 return generateRequestCode(conversation.getUuid(), actionId);
1557 }
1558
1559 private PendingIntent createDownloadIntent(final Message message) {
1560 return createContentIntent(message.getConversationUuid(), message.getUuid());
1561 }
1562
1563 private PendingIntent createContentIntent(final Conversational conversation) {
1564 return createContentIntent(conversation.getUuid(), null);
1565 }
1566
1567 private PendingIntent createDeleteIntent(final Conversation conversation) {
1568 final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
1569 intent.setAction(XmppConnectionService.ACTION_CLEAR_MESSAGE_NOTIFICATION);
1570 if (conversation != null) {
1571 intent.putExtra("uuid", conversation.getUuid());
1572 return PendingIntent.getService(
1573 mXmppConnectionService,
1574 generateRequestCode(conversation, 20),
1575 intent,
1576 s()
1577 ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1578 : PendingIntent.FLAG_UPDATE_CURRENT);
1579 }
1580 return PendingIntent.getService(
1581 mXmppConnectionService,
1582 0,
1583 intent,
1584 s()
1585 ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1586 : PendingIntent.FLAG_UPDATE_CURRENT);
1587 }
1588
1589 private PendingIntent createMissedCallsDeleteIntent(final Conversational conversation) {
1590 final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
1591 intent.setAction(XmppConnectionService.ACTION_CLEAR_MISSED_CALL_NOTIFICATION);
1592 if (conversation != null) {
1593 intent.putExtra("uuid", conversation.getUuid());
1594 return PendingIntent.getService(
1595 mXmppConnectionService,
1596 generateRequestCode(conversation, 21),
1597 intent,
1598 s()
1599 ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1600 : PendingIntent.FLAG_UPDATE_CURRENT);
1601 }
1602 return PendingIntent.getService(
1603 mXmppConnectionService,
1604 1,
1605 intent,
1606 s()
1607 ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1608 : PendingIntent.FLAG_UPDATE_CURRENT);
1609 }
1610
1611 private PendingIntent createReplyIntent(
1612 final Conversation conversation,
1613 final String lastMessageUuid,
1614 final boolean dismissAfterReply) {
1615 final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
1616 intent.setAction(XmppConnectionService.ACTION_REPLY_TO_CONVERSATION);
1617 intent.putExtra("uuid", conversation.getUuid());
1618 intent.putExtra("dismiss_notification", dismissAfterReply);
1619 intent.putExtra("last_message_uuid", lastMessageUuid);
1620 final int id = generateRequestCode(conversation, dismissAfterReply ? 12 : 14);
1621 return PendingIntent.getService(
1622 mXmppConnectionService,
1623 id,
1624 intent,
1625 s()
1626 ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1627 : PendingIntent.FLAG_UPDATE_CURRENT);
1628 }
1629
1630 private PendingIntent createReadPendingIntent(Conversation conversation) {
1631 final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
1632 intent.setAction(XmppConnectionService.ACTION_MARK_AS_READ);
1633 intent.putExtra("uuid", conversation.getUuid());
1634 intent.setPackage(mXmppConnectionService.getPackageName());
1635 return PendingIntent.getService(
1636 mXmppConnectionService,
1637 generateRequestCode(conversation, 16),
1638 intent,
1639 s()
1640 ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1641 : PendingIntent.FLAG_UPDATE_CURRENT);
1642 }
1643
1644 private PendingIntent createCallAction(String sessionId, final String action, int requestCode) {
1645 final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
1646 intent.setAction(action);
1647 intent.setPackage(mXmppConnectionService.getPackageName());
1648 intent.putExtra(RtpSessionActivity.EXTRA_SESSION_ID, sessionId);
1649 return PendingIntent.getService(
1650 mXmppConnectionService,
1651 requestCode,
1652 intent,
1653 s()
1654 ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1655 : PendingIntent.FLAG_UPDATE_CURRENT);
1656 }
1657
1658 private PendingIntent createSnoozeIntent(Conversation conversation) {
1659 final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
1660 intent.setAction(XmppConnectionService.ACTION_SNOOZE);
1661 intent.putExtra("uuid", conversation.getUuid());
1662 intent.setPackage(mXmppConnectionService.getPackageName());
1663 return PendingIntent.getService(
1664 mXmppConnectionService,
1665 generateRequestCode(conversation, 22),
1666 intent,
1667 s()
1668 ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1669 : PendingIntent.FLAG_UPDATE_CURRENT);
1670 }
1671
1672 private PendingIntent createTryAgainIntent() {
1673 final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
1674 intent.setAction(XmppConnectionService.ACTION_TRY_AGAIN);
1675 return PendingIntent.getService(
1676 mXmppConnectionService,
1677 45,
1678 intent,
1679 s()
1680 ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1681 : PendingIntent.FLAG_UPDATE_CURRENT);
1682 }
1683
1684 private PendingIntent createDismissErrorIntent() {
1685 final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
1686 intent.setAction(XmppConnectionService.ACTION_DISMISS_ERROR_NOTIFICATIONS);
1687 return PendingIntent.getService(
1688 mXmppConnectionService,
1689 69,
1690 intent,
1691 s()
1692 ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1693 : PendingIntent.FLAG_UPDATE_CURRENT);
1694 }
1695
1696 private boolean wasHighlightedOrPrivate(final Message message) {
1697 if (message.getConversation() instanceof Conversation) {
1698 Conversation conversation = (Conversation) message.getConversation();
1699 final String nick = conversation.getMucOptions().getActualNick();
1700 final Pattern highlight = generateNickHighlightPattern(nick);
1701 if (message.getBody() == null || nick == null) {
1702 return false;
1703 }
1704 final Matcher m = highlight.matcher(message.getBody());
1705 return (m.find() || message.isPrivateMessage());
1706 } else {
1707 return false;
1708 }
1709 }
1710
1711 public void setOpenConversation(final Conversation conversation) {
1712 this.mOpenConversation = conversation;
1713 }
1714
1715 public void setIsInForeground(final boolean foreground) {
1716 this.mIsInForeground = foreground;
1717 }
1718
1719 private int getPixel(final int dp) {
1720 final DisplayMetrics metrics = mXmppConnectionService.getResources().getDisplayMetrics();
1721 return ((int) (dp * metrics.density));
1722 }
1723
1724 private void markLastNotification() {
1725 this.mLastNotification = SystemClock.elapsedRealtime();
1726 }
1727
1728 private boolean inMiniGracePeriod(final Account account) {
1729 final int miniGrace =
1730 account.getStatus() == Account.State.ONLINE
1731 ? Config.MINI_GRACE_PERIOD
1732 : Config.MINI_GRACE_PERIOD * 2;
1733 return SystemClock.elapsedRealtime() < (this.mLastNotification + miniGrace);
1734 }
1735
1736 Notification createForegroundNotification() {
1737 final Notification.Builder mBuilder = new Notification.Builder(mXmppConnectionService);
1738 mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.app_name));
1739 final List<Account> accounts = mXmppConnectionService.getAccounts();
1740 int enabled = 0;
1741 int connected = 0;
1742 if (accounts != null) {
1743 for (Account account : accounts) {
1744 if (account.isOnlineAndConnected()) {
1745 connected++;
1746 enabled++;
1747 } else if (account.isEnabled()) {
1748 enabled++;
1749 }
1750 }
1751 }
1752 mBuilder.setContentText(
1753 mXmppConnectionService.getString(R.string.connected_accounts, connected, enabled));
1754 final PendingIntent openIntent = createOpenConversationsIntent();
1755 if (openIntent != null) {
1756 mBuilder.setContentIntent(openIntent);
1757 }
1758 mBuilder.setWhen(0)
1759 .setPriority(Notification.PRIORITY_MIN)
1760 .setSmallIcon(
1761 connected > 0
1762 ? R.drawable.ic_link_white_24dp
1763 : R.drawable.ic_link_off_white_24dp)
1764 .setLocalOnly(true);
1765
1766 if (Compatibility.runsTwentySix()) {
1767 mBuilder.setChannelId("foreground");
1768 }
1769
1770 return mBuilder.build();
1771 }
1772
1773 private PendingIntent createOpenConversationsIntent() {
1774 try {
1775 return PendingIntent.getActivity(
1776 mXmppConnectionService,
1777 0,
1778 new Intent(mXmppConnectionService, ConversationsActivity.class),
1779 s()
1780 ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1781 : PendingIntent.FLAG_UPDATE_CURRENT);
1782 } catch (RuntimeException e) {
1783 return null;
1784 }
1785 }
1786
1787 void updateErrorNotification() {
1788 if (Config.SUPPRESS_ERROR_NOTIFICATION) {
1789 cancel(ERROR_NOTIFICATION_ID);
1790 return;
1791 }
1792 final boolean showAllErrors = QuickConversationsService.isConversations();
1793 final List<Account> errors = new ArrayList<>();
1794 boolean torNotAvailable = false;
1795 for (final Account account : mXmppConnectionService.getAccounts()) {
1796 if (account.hasErrorStatus()
1797 && account.showErrorNotification()
1798 && (showAllErrors
1799 || account.getLastErrorStatus() == Account.State.UNAUTHORIZED)) {
1800 errors.add(account);
1801 torNotAvailable |= account.getStatus() == Account.State.TOR_NOT_AVAILABLE;
1802 }
1803 }
1804 if (mXmppConnectionService.foregroundNotificationNeedsUpdatingWhenErrorStateChanges()) {
1805 notify(FOREGROUND_NOTIFICATION_ID, createForegroundNotification());
1806 }
1807 final Notification.Builder mBuilder = new Notification.Builder(mXmppConnectionService);
1808 if (errors.size() == 0) {
1809 cancel(ERROR_NOTIFICATION_ID);
1810 return;
1811 } else if (errors.size() == 1) {
1812 mBuilder.setContentTitle(
1813 mXmppConnectionService.getString(R.string.problem_connecting_to_account));
1814 mBuilder.setContentText(errors.get(0).getJid().asBareJid().toEscapedString());
1815 } else {
1816 mBuilder.setContentTitle(
1817 mXmppConnectionService.getString(R.string.problem_connecting_to_accounts));
1818 mBuilder.setContentText(mXmppConnectionService.getString(R.string.touch_to_fix));
1819 }
1820 mBuilder.addAction(
1821 R.drawable.ic_autorenew_white_24dp,
1822 mXmppConnectionService.getString(R.string.try_again),
1823 createTryAgainIntent());
1824 if (torNotAvailable) {
1825 if (TorServiceUtils.isOrbotInstalled(mXmppConnectionService)) {
1826 mBuilder.addAction(
1827 R.drawable.ic_play_circle_filled_white_48dp,
1828 mXmppConnectionService.getString(R.string.start_orbot),
1829 PendingIntent.getActivity(
1830 mXmppConnectionService,
1831 147,
1832 TorServiceUtils.LAUNCH_INTENT,
1833 s()
1834 ? PendingIntent.FLAG_IMMUTABLE
1835 | PendingIntent.FLAG_UPDATE_CURRENT
1836 : PendingIntent.FLAG_UPDATE_CURRENT));
1837 } else {
1838 mBuilder.addAction(
1839 R.drawable.ic_file_download_white_24dp,
1840 mXmppConnectionService.getString(R.string.install_orbot),
1841 PendingIntent.getActivity(
1842 mXmppConnectionService,
1843 146,
1844 TorServiceUtils.INSTALL_INTENT,
1845 s()
1846 ? PendingIntent.FLAG_IMMUTABLE
1847 | PendingIntent.FLAG_UPDATE_CURRENT
1848 : PendingIntent.FLAG_UPDATE_CURRENT));
1849 }
1850 }
1851 mBuilder.setDeleteIntent(createDismissErrorIntent());
1852 mBuilder.setVisibility(Notification.VISIBILITY_PRIVATE);
1853 mBuilder.setSmallIcon(R.drawable.ic_warning_white_24dp);
1854 mBuilder.setLocalOnly(true);
1855 mBuilder.setPriority(Notification.PRIORITY_LOW);
1856 final Intent intent;
1857 if (AccountUtils.MANAGE_ACCOUNT_ACTIVITY != null) {
1858 intent = new Intent(mXmppConnectionService, AccountUtils.MANAGE_ACCOUNT_ACTIVITY);
1859 } else {
1860 intent = new Intent(mXmppConnectionService, EditAccountActivity.class);
1861 intent.putExtra("jid", errors.get(0).getJid().asBareJid().toEscapedString());
1862 intent.putExtra(EditAccountActivity.EXTRA_OPENED_FROM_NOTIFICATION, true);
1863 }
1864 mBuilder.setContentIntent(
1865 PendingIntent.getActivity(
1866 mXmppConnectionService,
1867 145,
1868 intent,
1869 s()
1870 ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1871 : PendingIntent.FLAG_UPDATE_CURRENT));
1872 if (Compatibility.runsTwentySix()) {
1873 mBuilder.setChannelId("error");
1874 }
1875 notify(ERROR_NOTIFICATION_ID, mBuilder.build());
1876 }
1877
1878 void updateFileAddingNotification(int current, Message message) {
1879 Notification.Builder mBuilder = new Notification.Builder(mXmppConnectionService);
1880 mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.transcoding_video));
1881 mBuilder.setProgress(100, current, false);
1882 mBuilder.setSmallIcon(R.drawable.ic_hourglass_empty_white_24dp);
1883 mBuilder.setContentIntent(createContentIntent(message.getConversation()));
1884 mBuilder.setOngoing(true);
1885 if (Compatibility.runsTwentySix()) {
1886 mBuilder.setChannelId("compression");
1887 }
1888 Notification notification = mBuilder.build();
1889 notify(FOREGROUND_NOTIFICATION_ID, notification);
1890 }
1891
1892 private void notify(String tag, int id, Notification notification) {
1893 final NotificationManagerCompat notificationManager =
1894 NotificationManagerCompat.from(mXmppConnectionService);
1895 try {
1896 notificationManager.notify(tag, id, notification);
1897 } catch (RuntimeException e) {
1898 Log.d(Config.LOGTAG, "unable to make notification", e);
1899 }
1900 }
1901
1902 public void notify(int id, Notification notification) {
1903 final NotificationManagerCompat notificationManager =
1904 NotificationManagerCompat.from(mXmppConnectionService);
1905 try {
1906 notificationManager.notify(id, notification);
1907 } catch (RuntimeException e) {
1908 Log.d(Config.LOGTAG, "unable to make notification", e);
1909 }
1910 }
1911
1912 public void cancel(int id) {
1913 final NotificationManagerCompat notificationManager =
1914 NotificationManagerCompat.from(mXmppConnectionService);
1915 try {
1916 notificationManager.cancel(id);
1917 } catch (RuntimeException e) {
1918 Log.d(Config.LOGTAG, "unable to cancel notification", e);
1919 }
1920 }
1921
1922 private void cancel(String tag, int id) {
1923 final NotificationManagerCompat notificationManager =
1924 NotificationManagerCompat.from(mXmppConnectionService);
1925 try {
1926 notificationManager.cancel(tag, id);
1927 } catch (RuntimeException e) {
1928 Log.d(Config.LOGTAG, "unable to cancel notification", e);
1929 }
1930 }
1931
1932 private static class MissedCallsInfo {
1933 private int numberOfCalls;
1934 private long lastTime;
1935
1936 MissedCallsInfo(final long time) {
1937 numberOfCalls = 1;
1938 lastTime = time;
1939 }
1940
1941 public void newMissedCall(final long time) {
1942 ++numberOfCalls;
1943 lastTime = time;
1944 }
1945
1946 public int getNumberOfCalls() {
1947 return numberOfCalls;
1948 }
1949
1950 public long getLastTime() {
1951 return lastTime;
1952 }
1953 }
1954
1955 private class VibrationRunnable implements Runnable {
1956
1957 @Override
1958 public void run() {
1959 final Vibrator vibrator =
1960 (Vibrator) mXmppConnectionService.getSystemService(Context.VIBRATOR_SERVICE);
1961 vibrator.vibrate(CALL_PATTERN, -1);
1962 }
1963 }
1964}