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