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