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