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 AbstractJingleConnection.Id id, final Set<Media> media) {
492 final NotificationCompat.Builder builder = new NotificationCompat.Builder(mXmppConnectionService, "ongoing_calls");
493 if (media.contains(Media.VIDEO)) {
494 builder.setSmallIcon(R.drawable.ic_videocam_white_24dp);
495 builder.setContentTitle(mXmppConnectionService.getString(R.string.ongoing_video_call));
496 } else {
497 builder.setSmallIcon(R.drawable.ic_call_white_24dp);
498 builder.setContentTitle(mXmppConnectionService.getString(R.string.ongoing_call));
499 }
500 builder.setContentText(id.account.getRoster().getContact(id.with).getDisplayName());
501 builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC);
502 builder.setPriority(NotificationCompat.PRIORITY_HIGH);
503 builder.setCategory(NotificationCompat.CATEGORY_CALL);
504 builder.setContentIntent(createPendingRtpSession(id, Intent.ACTION_VIEW, 101));
505 builder.setOngoing(true);
506 builder.addAction(new NotificationCompat.Action.Builder(
507 R.drawable.ic_call_end_white_48dp,
508 mXmppConnectionService.getString(R.string.hang_up),
509 createCallAction(id.sessionId, XmppConnectionService.ACTION_END_CALL, 104))
510 .build());
511 return builder.build();
512 }
513
514 private PendingIntent createPendingRtpSession(final AbstractJingleConnection.Id id, final String action, final int requestCode) {
515 final Intent fullScreenIntent = new Intent(mXmppConnectionService, RtpSessionActivity.class);
516 fullScreenIntent.setAction(action);
517 fullScreenIntent.putExtra(RtpSessionActivity.EXTRA_ACCOUNT, id.account.getJid().asBareJid().toEscapedString());
518 fullScreenIntent.putExtra(RtpSessionActivity.EXTRA_WITH, id.with.toEscapedString());
519 fullScreenIntent.putExtra(RtpSessionActivity.EXTRA_SESSION_ID, id.sessionId);
520 return PendingIntent.getActivity(mXmppConnectionService, requestCode, fullScreenIntent, PendingIntent.FLAG_UPDATE_CURRENT);
521 }
522
523 public void cancelIncomingCallNotification() {
524 stopSoundAndVibration();
525 cancel(INCOMING_CALL_NOTIFICATION_ID);
526 }
527
528 public boolean stopSoundAndVibration() {
529 int stopped = 0;
530 if (this.currentlyPlayingRingtone != null) {
531 if (this.currentlyPlayingRingtone.isPlaying()) {
532 Log.d(Config.LOGTAG, "stop playing ring tone");
533 ++stopped;
534 }
535 this.currentlyPlayingRingtone.stop();
536 }
537 if (this.vibrationFuture != null && !this.vibrationFuture.isCancelled()) {
538 Log.d(Config.LOGTAG, "stop vibration");
539 this.vibrationFuture.cancel(true);
540 ++stopped;
541 }
542 return stopped > 0;
543 }
544
545 public static void cancelIncomingCallNotification(final Context context) {
546 final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
547 try {
548 notificationManager.cancel(INCOMING_CALL_NOTIFICATION_ID);
549 } catch (RuntimeException e) {
550 Log.d(Config.LOGTAG, "unable to cancel incoming call notification after crash", e);
551 }
552 }
553
554 private void pushNow(final Message message) {
555 mXmppConnectionService.updateUnreadCountBadge();
556 if (!notify(message)) {
557 Log.d(Config.LOGTAG, message.getConversation().getAccount().getJid().asBareJid() + ": suppressing notification because turned off");
558 return;
559 }
560 final boolean isScreenLocked = mXmppConnectionService.isScreenLocked();
561 if (this.mIsInForeground && !isScreenLocked && this.mOpenConversation == message.getConversation()) {
562 Log.d(Config.LOGTAG, message.getConversation().getAccount().getJid().asBareJid() + ": suppressing notification because conversation is open");
563 return;
564 }
565 synchronized (notifications) {
566 pushToStack(message);
567 final Conversational conversation = message.getConversation();
568 final Account account = conversation.getAccount();
569 final boolean doNotify = (!(this.mIsInForeground && this.mOpenConversation == null) || isScreenLocked)
570 && !account.inGracePeriod()
571 && !this.inMiniGracePeriod(account);
572 updateNotification(doNotify, Collections.singletonList(conversation.getUuid()));
573 }
574 }
575
576 public void clear() {
577 synchronized (notifications) {
578 for (ArrayList<Message> messages : notifications.values()) {
579 markAsReadIfHasDirectReply(messages);
580 }
581 notifications.clear();
582 updateNotification(false);
583 }
584 }
585
586 public void clear(final Conversation conversation) {
587 synchronized (this.mBacklogMessageCounter) {
588 this.mBacklogMessageCounter.remove(conversation);
589 }
590 synchronized (notifications) {
591 markAsReadIfHasDirectReply(conversation);
592 if (notifications.remove(conversation.getUuid()) != null) {
593 cancel(conversation.getUuid(), NOTIFICATION_ID);
594 updateNotification(false, null, true);
595 }
596 }
597 }
598
599 private void markAsReadIfHasDirectReply(final Conversation conversation) {
600 markAsReadIfHasDirectReply(notifications.get(conversation.getUuid()));
601 }
602
603 private void markAsReadIfHasDirectReply(final ArrayList<Message> messages) {
604 if (messages != null && messages.size() > 0) {
605 Message last = messages.get(messages.size() - 1);
606 if (last.getStatus() != Message.STATUS_RECEIVED) {
607 if (mXmppConnectionService.markRead((Conversation) last.getConversation(), false)) {
608 mXmppConnectionService.updateConversationUi();
609 }
610 }
611 }
612 }
613
614 private void setNotificationColor(final Builder mBuilder) {
615 mBuilder.setColor(ContextCompat.getColor(mXmppConnectionService, R.color.green600));
616 }
617
618 public void updateNotification() {
619 synchronized (notifications) {
620 updateNotification(false);
621 }
622 }
623
624 private void updateNotification(final boolean notify) {
625 updateNotification(notify, null, false);
626 }
627
628 private void updateNotification(final boolean notify, final List<String> conversations) {
629 updateNotification(notify, conversations, false);
630 }
631
632 private void updateNotification(final boolean notify, final List<String> conversations, final boolean summaryOnly) {
633 final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mXmppConnectionService);
634
635 final boolean quiteHours = isQuietHours();
636
637 final boolean notifyOnlyOneChild = notify && conversations != null && conversations.size() == 1; //if this check is changed to > 0 catchup messages will create one notification per conversation
638
639
640 if (notifications.size() == 0) {
641 cancel(NOTIFICATION_ID);
642 } else {
643 if (notify) {
644 this.markLastNotification();
645 }
646 final Builder mBuilder;
647 if (notifications.size() == 1 && Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
648 mBuilder = buildSingleConversations(notifications.values().iterator().next(), notify, quiteHours);
649 modifyForSoundVibrationAndLight(mBuilder, notify, quiteHours, preferences);
650 notify(NOTIFICATION_ID, mBuilder.build());
651 } else {
652 mBuilder = buildMultipleConversation(notify, quiteHours);
653 if (notifyOnlyOneChild) {
654 mBuilder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN);
655 }
656 modifyForSoundVibrationAndLight(mBuilder, notify, quiteHours, preferences);
657 if (!summaryOnly) {
658 for (Map.Entry<String, ArrayList<Message>> entry : notifications.entrySet()) {
659 String uuid = entry.getKey();
660 final boolean notifyThis = notifyOnlyOneChild ? conversations.contains(uuid) : notify;
661 Builder singleBuilder = buildSingleConversations(entry.getValue(), notifyThis, quiteHours);
662 if (!notifyOnlyOneChild) {
663 singleBuilder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY);
664 }
665 modifyForSoundVibrationAndLight(singleBuilder, notifyThis, quiteHours, preferences);
666 singleBuilder.setGroup(CONVERSATIONS_GROUP);
667 setNotificationColor(singleBuilder);
668 notify(entry.getKey(), NOTIFICATION_ID, singleBuilder.build());
669 }
670 }
671 notify(NOTIFICATION_ID, mBuilder.build());
672 }
673 }
674 }
675
676 private void modifyForSoundVibrationAndLight(Builder mBuilder, boolean notify, boolean quietHours, SharedPreferences preferences) {
677 final Resources resources = mXmppConnectionService.getResources();
678 final String ringtone = preferences.getString("notification_ringtone", resources.getString(R.string.notification_ringtone));
679 final boolean vibrate = preferences.getBoolean("vibrate_on_notification", resources.getBoolean(R.bool.vibrate_on_notification));
680 final boolean led = preferences.getBoolean("led", resources.getBoolean(R.bool.led));
681 final boolean headsup = preferences.getBoolean("notification_headsup", resources.getBoolean(R.bool.headsup_notifications));
682 if (notify && !quietHours) {
683 if (vibrate) {
684 final int dat = 70;
685 final long[] pattern = {0, 3 * dat, dat, dat};
686 mBuilder.setVibrate(pattern);
687 } else {
688 mBuilder.setVibrate(new long[]{0});
689 }
690 Uri uri = Uri.parse(ringtone);
691 try {
692 mBuilder.setSound(fixRingtoneUri(uri));
693 } catch (SecurityException e) {
694 Log.d(Config.LOGTAG, "unable to use custom notification sound " + uri.toString());
695 }
696 } else {
697 mBuilder.setLocalOnly(true);
698 }
699 if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
700 mBuilder.setCategory(Notification.CATEGORY_MESSAGE);
701 }
702 mBuilder.setPriority(notify ? (headsup ? NotificationCompat.PRIORITY_HIGH : NotificationCompat.PRIORITY_DEFAULT) : NotificationCompat.PRIORITY_LOW);
703 setNotificationColor(mBuilder);
704 mBuilder.setDefaults(0);
705 if (led) {
706 mBuilder.setLights(LED_COLOR, 2000, 3000);
707 }
708 }
709
710 private void modifyIncomingCall(final Builder mBuilder) {
711 mBuilder.setPriority(NotificationCompat.PRIORITY_HIGH);
712 setNotificationColor(mBuilder);
713 mBuilder.setLights(LED_COLOR, 2000, 3000);
714 }
715
716 private Uri fixRingtoneUri(Uri uri) {
717 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && "file".equals(uri.getScheme())) {
718 return FileBackend.getUriForFile(mXmppConnectionService, new File(uri.getPath()));
719 } else {
720 return uri;
721 }
722 }
723
724 private Builder buildMultipleConversation(final boolean notify, final boolean quietHours) {
725 final Builder mBuilder = new NotificationCompat.Builder(mXmppConnectionService, quietHours ? "quiet_hours" : (notify ? "messages" : "silent_messages"));
726 final NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle();
727 style.setBigContentTitle(mXmppConnectionService.getResources().getQuantityString(R.plurals.x_unread_conversations, notifications.size(), notifications.size()));
728 final StringBuilder names = new StringBuilder();
729 Conversation conversation = null;
730 for (final ArrayList<Message> messages : notifications.values()) {
731 if (messages.size() > 0) {
732 conversation = (Conversation) messages.get(0).getConversation();
733 final String name = conversation.getName().toString();
734 SpannableString styledString;
735 if (Config.HIDE_MESSAGE_TEXT_IN_NOTIFICATION) {
736 int count = messages.size();
737 styledString = new SpannableString(name + ": " + mXmppConnectionService.getResources().getQuantityString(R.plurals.x_messages, count, count));
738 styledString.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), 0);
739 style.addLine(styledString);
740 } else {
741 styledString = new SpannableString(name + ": " + UIHelper.getMessagePreview(mXmppConnectionService, messages.get(0)).first);
742 styledString.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), 0);
743 style.addLine(styledString);
744 }
745 names.append(name);
746 names.append(", ");
747 }
748 }
749 if (names.length() >= 2) {
750 names.delete(names.length() - 2, names.length());
751 }
752 final String contentTitle = mXmppConnectionService.getResources().getQuantityString(R.plurals.x_unread_conversations, notifications.size(), notifications.size());
753 mBuilder.setContentTitle(contentTitle);
754 mBuilder.setTicker(contentTitle);
755 mBuilder.setContentText(names.toString());
756 mBuilder.setStyle(style);
757 if (conversation != null) {
758 mBuilder.setContentIntent(createContentIntent(conversation));
759 }
760 mBuilder.setGroupSummary(true);
761 mBuilder.setGroup(CONVERSATIONS_GROUP);
762 mBuilder.setDeleteIntent(createDeleteIntent(null));
763 mBuilder.setSmallIcon(R.drawable.ic_notification);
764 return mBuilder;
765 }
766
767 private Builder buildSingleConversations(final ArrayList<Message> messages, final boolean notify, final boolean quietHours) {
768 final Builder mBuilder = new NotificationCompat.Builder(mXmppConnectionService, quietHours ? "quiet_hours" : (notify ? "messages" : "silent_messages"));
769 if (messages.size() >= 1) {
770 final Conversation conversation = (Conversation) messages.get(0).getConversation();
771 mBuilder.setLargeIcon(mXmppConnectionService.getAvatarService()
772 .get(conversation, AvatarService.getSystemUiAvatarSize(mXmppConnectionService)));
773 mBuilder.setContentTitle(conversation.getName());
774 if (Config.HIDE_MESSAGE_TEXT_IN_NOTIFICATION) {
775 int count = messages.size();
776 mBuilder.setContentText(mXmppConnectionService.getResources().getQuantityString(R.plurals.x_messages, count, count));
777 } else {
778 Message message;
779 //TODO starting with Android 9 we might want to put images in MessageStyle
780 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P && (message = getImage(messages)) != null) {
781 modifyForImage(mBuilder, message, messages);
782 } else {
783 modifyForTextOnly(mBuilder, messages);
784 }
785 RemoteInput remoteInput = new RemoteInput.Builder("text_reply").setLabel(UIHelper.getMessageHint(mXmppConnectionService, conversation)).build();
786 PendingIntent markAsReadPendingIntent = createReadPendingIntent(conversation);
787 NotificationCompat.Action markReadAction = new NotificationCompat.Action.Builder(
788 R.drawable.ic_drafts_white_24dp,
789 mXmppConnectionService.getString(R.string.mark_as_read),
790 markAsReadPendingIntent)
791 .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_MARK_AS_READ)
792 .setShowsUserInterface(false)
793 .build();
794 final String replyLabel = mXmppConnectionService.getString(R.string.reply);
795 final String lastMessageUuid = Iterables.getLast(messages).getUuid();
796 final NotificationCompat.Action replyAction = new NotificationCompat.Action.Builder(
797 R.drawable.ic_send_text_offline,
798 replyLabel,
799 createReplyIntent(conversation, lastMessageUuid, false))
800 .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY)
801 .setShowsUserInterface(false)
802 .addRemoteInput(remoteInput).build();
803 final NotificationCompat.Action wearReplyAction = new NotificationCompat.Action.Builder(R.drawable.ic_wear_reply,
804 replyLabel,
805 createReplyIntent(conversation, lastMessageUuid, true)).addRemoteInput(remoteInput).build();
806 mBuilder.extend(new NotificationCompat.WearableExtender().addAction(wearReplyAction));
807 int addedActionsCount = 1;
808 mBuilder.addAction(markReadAction);
809 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
810 mBuilder.addAction(replyAction);
811 ++addedActionsCount;
812 }
813
814 if (displaySnoozeAction(messages)) {
815 String label = mXmppConnectionService.getString(R.string.snooze);
816 PendingIntent pendingSnoozeIntent = createSnoozeIntent(conversation);
817 NotificationCompat.Action snoozeAction = new NotificationCompat.Action.Builder(
818 R.drawable.ic_notifications_paused_white_24dp,
819 label,
820 pendingSnoozeIntent).build();
821 mBuilder.addAction(snoozeAction);
822 ++addedActionsCount;
823 }
824 if (addedActionsCount < 3) {
825 final Message firstLocationMessage = getFirstLocationMessage(messages);
826 if (firstLocationMessage != null) {
827 final PendingIntent pendingShowLocationIntent = createShowLocationIntent(firstLocationMessage);
828 if (pendingShowLocationIntent != null) {
829 final String label = mXmppConnectionService.getResources().getString(R.string.show_location);
830 NotificationCompat.Action locationAction = new NotificationCompat.Action.Builder(
831 R.drawable.ic_room_white_24dp,
832 label,
833 pendingShowLocationIntent).build();
834 mBuilder.addAction(locationAction);
835 ++addedActionsCount;
836 }
837 }
838 }
839 if (addedActionsCount < 3) {
840 Message firstDownloadableMessage = getFirstDownloadableMessage(messages);
841 if (firstDownloadableMessage != null) {
842 String label = mXmppConnectionService.getResources().getString(R.string.download_x_file, UIHelper.getFileDescriptionString(mXmppConnectionService, firstDownloadableMessage));
843 PendingIntent pendingDownloadIntent = createDownloadIntent(firstDownloadableMessage);
844 NotificationCompat.Action downloadAction = new NotificationCompat.Action.Builder(
845 R.drawable.ic_file_download_white_24dp,
846 label,
847 pendingDownloadIntent).build();
848 mBuilder.addAction(downloadAction);
849 ++addedActionsCount;
850 }
851 }
852 }
853 if (conversation.getMode() == Conversation.MODE_SINGLE) {
854 Contact contact = conversation.getContact();
855 Uri systemAccount = contact.getSystemAccount();
856 if (systemAccount != null) {
857 mBuilder.addPerson(systemAccount.toString());
858 }
859 }
860 mBuilder.setWhen(conversation.getLatestMessage().getTimeSent());
861 mBuilder.setSmallIcon(R.drawable.ic_notification);
862 mBuilder.setDeleteIntent(createDeleteIntent(conversation));
863 mBuilder.setContentIntent(createContentIntent(conversation));
864 }
865 return mBuilder;
866 }
867
868 private void modifyForImage(final Builder builder, final Message message, final ArrayList<Message> messages) {
869 try {
870 final Bitmap bitmap = mXmppConnectionService.getFileBackend().getThumbnail(message, getPixel(288), false);
871 final ArrayList<Message> tmp = new ArrayList<>();
872 for (final Message msg : messages) {
873 if (msg.getType() == Message.TYPE_TEXT
874 && msg.getTransferable() == null) {
875 tmp.add(msg);
876 }
877 }
878 final BigPictureStyle bigPictureStyle = new NotificationCompat.BigPictureStyle();
879 bigPictureStyle.bigPicture(bitmap);
880 if (tmp.size() > 0) {
881 CharSequence text = getMergedBodies(tmp);
882 bigPictureStyle.setSummaryText(text);
883 builder.setContentText(text);
884 builder.setTicker(text);
885 } else {
886 final String description = UIHelper.getFileDescriptionString(mXmppConnectionService, message);
887 builder.setContentText(description);
888 builder.setTicker(description);
889 }
890 builder.setStyle(bigPictureStyle);
891 } catch (final IOException e) {
892 modifyForTextOnly(builder, messages);
893 }
894 }
895
896 private Person getPerson(Message message) {
897 final Contact contact = message.getContact();
898 final Person.Builder builder = new Person.Builder();
899 if (contact != null) {
900 builder.setName(contact.getDisplayName());
901 final Uri uri = contact.getSystemAccount();
902 if (uri != null) {
903 builder.setUri(uri.toString());
904 }
905 } else {
906 builder.setName(UIHelper.getMessageDisplayName(message));
907 }
908 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
909 builder.setIcon(IconCompat.createWithBitmap(mXmppConnectionService.getAvatarService().get(message, AvatarService.getSystemUiAvatarSize(mXmppConnectionService), false)));
910 }
911 return builder.build();
912 }
913
914 private void modifyForTextOnly(final Builder builder, final ArrayList<Message> messages) {
915 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
916 final Conversation conversation = (Conversation) messages.get(0).getConversation();
917 final Person.Builder meBuilder = new Person.Builder().setName(mXmppConnectionService.getString(R.string.me));
918 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
919 meBuilder.setIcon(IconCompat.createWithBitmap(mXmppConnectionService.getAvatarService().get(conversation.getAccount(), AvatarService.getSystemUiAvatarSize(mXmppConnectionService))));
920 }
921 final Person me = meBuilder.build();
922 NotificationCompat.MessagingStyle messagingStyle = new NotificationCompat.MessagingStyle(me);
923 final boolean multiple = conversation.getMode() == Conversation.MODE_MULTI;
924 if (multiple) {
925 messagingStyle.setConversationTitle(conversation.getName());
926 }
927 for (Message message : messages) {
928 final Person sender = message.getStatus() == Message.STATUS_RECEIVED ? getPerson(message) : null;
929 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && isImageMessage(message)) {
930 final Uri dataUri = FileBackend.getMediaUri(mXmppConnectionService, mXmppConnectionService.getFileBackend().getFile(message));
931 NotificationCompat.MessagingStyle.Message imageMessage = new NotificationCompat.MessagingStyle.Message(UIHelper.getMessagePreview(mXmppConnectionService, message).first, message.getTimeSent(), sender);
932 if (dataUri != null) {
933 imageMessage.setData(message.getMimeType(), dataUri);
934 }
935 messagingStyle.addMessage(imageMessage);
936 } else {
937 messagingStyle.addMessage(UIHelper.getMessagePreview(mXmppConnectionService, message).first, message.getTimeSent(), sender);
938 }
939 }
940 messagingStyle.setGroupConversation(multiple);
941 builder.setStyle(messagingStyle);
942 } else {
943 if (messages.get(0).getConversation().getMode() == Conversation.MODE_SINGLE) {
944 builder.setStyle(new NotificationCompat.BigTextStyle().bigText(getMergedBodies(messages)));
945 final CharSequence preview = UIHelper.getMessagePreview(mXmppConnectionService, messages.get(messages.size() - 1)).first;
946 builder.setContentText(preview);
947 builder.setTicker(preview);
948 builder.setNumber(messages.size());
949 } else {
950 final NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle();
951 SpannableString styledString;
952 for (Message message : messages) {
953 final String name = UIHelper.getMessageDisplayName(message);
954 styledString = new SpannableString(name + ": " + message.getBody());
955 styledString.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), 0);
956 style.addLine(styledString);
957 }
958 builder.setStyle(style);
959 int count = messages.size();
960 if (count == 1) {
961 final String name = UIHelper.getMessageDisplayName(messages.get(0));
962 styledString = new SpannableString(name + ": " + messages.get(0).getBody());
963 styledString.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), 0);
964 builder.setContentText(styledString);
965 builder.setTicker(styledString);
966 } else {
967 final String text = mXmppConnectionService.getResources().getQuantityString(R.plurals.x_messages, count, count);
968 builder.setContentText(text);
969 builder.setTicker(text);
970 }
971 }
972 }
973 }
974
975 private Message getImage(final Iterable<Message> messages) {
976 Message image = null;
977 for (final Message message : messages) {
978 if (message.getStatus() != Message.STATUS_RECEIVED) {
979 return null;
980 }
981 if (isImageMessage(message)) {
982 image = message;
983 }
984 }
985 return image;
986 }
987
988 private Message getFirstDownloadableMessage(final Iterable<Message> messages) {
989 for (final Message message : messages) {
990 if (message.getTransferable() != null || (message.getType() == Message.TYPE_TEXT && message.treatAsDownloadable())) {
991 return message;
992 }
993 }
994 return null;
995 }
996
997 private Message getFirstLocationMessage(final Iterable<Message> messages) {
998 for (final Message message : messages) {
999 if (message.isGeoUri()) {
1000 return message;
1001 }
1002 }
1003 return null;
1004 }
1005
1006 private CharSequence getMergedBodies(final ArrayList<Message> messages) {
1007 final StringBuilder text = new StringBuilder();
1008 for (Message message : messages) {
1009 if (text.length() != 0) {
1010 text.append("\n");
1011 }
1012 text.append(UIHelper.getMessagePreview(mXmppConnectionService, message).first);
1013 }
1014 return text.toString();
1015 }
1016
1017 private PendingIntent createShowLocationIntent(final Message message) {
1018 Iterable<Intent> intents = GeoHelper.createGeoIntentsFromMessage(mXmppConnectionService, message);
1019 for (Intent intent : intents) {
1020 if (intent.resolveActivity(mXmppConnectionService.getPackageManager()) != null) {
1021 return PendingIntent.getActivity(mXmppConnectionService, generateRequestCode(message.getConversation(), 18), intent, PendingIntent.FLAG_UPDATE_CURRENT);
1022 }
1023 }
1024 return null;
1025 }
1026
1027 private PendingIntent createContentIntent(final String conversationUuid, final String downloadMessageUuid) {
1028 final Intent viewConversationIntent = new Intent(mXmppConnectionService, ConversationsActivity.class);
1029 viewConversationIntent.setAction(ConversationsActivity.ACTION_VIEW_CONVERSATION);
1030 viewConversationIntent.putExtra(ConversationsActivity.EXTRA_CONVERSATION, conversationUuid);
1031 if (downloadMessageUuid != null) {
1032 viewConversationIntent.putExtra(ConversationsActivity.EXTRA_DOWNLOAD_UUID, downloadMessageUuid);
1033 return PendingIntent.getActivity(mXmppConnectionService,
1034 generateRequestCode(conversationUuid, 8),
1035 viewConversationIntent,
1036 PendingIntent.FLAG_UPDATE_CURRENT);
1037 } else {
1038 return PendingIntent.getActivity(mXmppConnectionService,
1039 generateRequestCode(conversationUuid, 10),
1040 viewConversationIntent,
1041 PendingIntent.FLAG_UPDATE_CURRENT);
1042 }
1043 }
1044
1045 private int generateRequestCode(String uuid, int actionId) {
1046 return (actionId * NOTIFICATION_ID_MULTIPLIER) + (uuid.hashCode() % NOTIFICATION_ID_MULTIPLIER);
1047 }
1048
1049 private int generateRequestCode(Conversational conversation, int actionId) {
1050 return generateRequestCode(conversation.getUuid(), actionId);
1051 }
1052
1053 private PendingIntent createDownloadIntent(final Message message) {
1054 return createContentIntent(message.getConversationUuid(), message.getUuid());
1055 }
1056
1057 private PendingIntent createContentIntent(final Conversational conversation) {
1058 return createContentIntent(conversation.getUuid(), null);
1059 }
1060
1061 private PendingIntent createDeleteIntent(Conversation conversation) {
1062 final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
1063 intent.setAction(XmppConnectionService.ACTION_CLEAR_NOTIFICATION);
1064 if (conversation != null) {
1065 intent.putExtra("uuid", conversation.getUuid());
1066 return PendingIntent.getService(mXmppConnectionService, generateRequestCode(conversation, 20), intent, 0);
1067 }
1068 return PendingIntent.getService(mXmppConnectionService, 0, intent, 0);
1069 }
1070
1071 private PendingIntent createReplyIntent(final Conversation conversation, final String lastMessageUuid, final boolean dismissAfterReply) {
1072 final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
1073 intent.setAction(XmppConnectionService.ACTION_REPLY_TO_CONVERSATION);
1074 intent.putExtra("uuid", conversation.getUuid());
1075 intent.putExtra("dismiss_notification", dismissAfterReply);
1076 intent.putExtra("last_message_uuid", lastMessageUuid);
1077 final int id = generateRequestCode(conversation, dismissAfterReply ? 12 : 14);
1078 return PendingIntent.getService(mXmppConnectionService, id, intent, PendingIntent.FLAG_UPDATE_CURRENT);
1079 }
1080
1081 private PendingIntent createReadPendingIntent(Conversation conversation) {
1082 final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
1083 intent.setAction(XmppConnectionService.ACTION_MARK_AS_READ);
1084 intent.putExtra("uuid", conversation.getUuid());
1085 intent.setPackage(mXmppConnectionService.getPackageName());
1086 return PendingIntent.getService(mXmppConnectionService, generateRequestCode(conversation, 16), intent, PendingIntent.FLAG_UPDATE_CURRENT);
1087 }
1088
1089 private PendingIntent createCallAction(String sessionId, final String action, int requestCode) {
1090 final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
1091 intent.setAction(action);
1092 intent.setPackage(mXmppConnectionService.getPackageName());
1093 intent.putExtra(RtpSessionActivity.EXTRA_SESSION_ID, sessionId);
1094 return PendingIntent.getService(mXmppConnectionService, requestCode, intent, PendingIntent.FLAG_UPDATE_CURRENT);
1095 }
1096
1097 private PendingIntent createSnoozeIntent(Conversation conversation) {
1098 final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
1099 intent.setAction(XmppConnectionService.ACTION_SNOOZE);
1100 intent.putExtra("uuid", conversation.getUuid());
1101 intent.setPackage(mXmppConnectionService.getPackageName());
1102 return PendingIntent.getService(mXmppConnectionService, generateRequestCode(conversation, 22), intent, PendingIntent.FLAG_UPDATE_CURRENT);
1103 }
1104
1105 private PendingIntent createTryAgainIntent() {
1106 final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
1107 intent.setAction(XmppConnectionService.ACTION_TRY_AGAIN);
1108 return PendingIntent.getService(mXmppConnectionService, 45, intent, 0);
1109 }
1110
1111 private PendingIntent createDismissErrorIntent() {
1112 final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
1113 intent.setAction(XmppConnectionService.ACTION_DISMISS_ERROR_NOTIFICATIONS);
1114 return PendingIntent.getService(mXmppConnectionService, 69, intent, 0);
1115 }
1116
1117 private boolean wasHighlightedOrPrivate(final Message message) {
1118 if (message.getConversation() instanceof Conversation) {
1119 Conversation conversation = (Conversation) message.getConversation();
1120 final String nick = conversation.getMucOptions().getActualNick();
1121 final Pattern highlight = generateNickHighlightPattern(nick);
1122 if (message.getBody() == null || nick == null) {
1123 return false;
1124 }
1125 final Matcher m = highlight.matcher(message.getBody());
1126 return (m.find() || message.isPrivateMessage());
1127 } else {
1128 return false;
1129 }
1130 }
1131
1132 public void setOpenConversation(final Conversation conversation) {
1133 this.mOpenConversation = conversation;
1134 }
1135
1136 public void setIsInForeground(final boolean foreground) {
1137 this.mIsInForeground = foreground;
1138 }
1139
1140 private int getPixel(final int dp) {
1141 final DisplayMetrics metrics = mXmppConnectionService.getResources()
1142 .getDisplayMetrics();
1143 return ((int) (dp * metrics.density));
1144 }
1145
1146 private void markLastNotification() {
1147 this.mLastNotification = SystemClock.elapsedRealtime();
1148 }
1149
1150 private boolean inMiniGracePeriod(final Account account) {
1151 final int miniGrace = account.getStatus() == Account.State.ONLINE ? Config.MINI_GRACE_PERIOD
1152 : Config.MINI_GRACE_PERIOD * 2;
1153 return SystemClock.elapsedRealtime() < (this.mLastNotification + miniGrace);
1154 }
1155
1156 Notification createForegroundNotification() {
1157 final Notification.Builder mBuilder = new Notification.Builder(mXmppConnectionService);
1158 mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.app_name));
1159 final List<Account> accounts = mXmppConnectionService.getAccounts();
1160 int enabled = 0;
1161 int connected = 0;
1162 if (accounts != null) {
1163 for (Account account : accounts) {
1164 if (account.isOnlineAndConnected()) {
1165 connected++;
1166 enabled++;
1167 } else if (account.isEnabled()) {
1168 enabled++;
1169 }
1170 }
1171 }
1172 mBuilder.setContentText(mXmppConnectionService.getString(R.string.connected_accounts, connected, enabled));
1173 final PendingIntent openIntent = createOpenConversationsIntent();
1174 if (openIntent != null) {
1175 mBuilder.setContentIntent(openIntent);
1176 }
1177 mBuilder.setWhen(0);
1178 mBuilder.setPriority(Notification.PRIORITY_MIN);
1179 mBuilder.setSmallIcon(connected > 0 ? R.drawable.ic_link_white_24dp : R.drawable.ic_link_off_white_24dp);
1180
1181 if (Compatibility.runsTwentySix()) {
1182 mBuilder.setChannelId("foreground");
1183 }
1184
1185
1186 return mBuilder.build();
1187 }
1188
1189 private PendingIntent createOpenConversationsIntent() {
1190 try {
1191 return PendingIntent.getActivity(mXmppConnectionService, 0, new Intent(mXmppConnectionService, ConversationsActivity.class), 0);
1192 } catch (RuntimeException e) {
1193 return null;
1194 }
1195 }
1196
1197 void updateErrorNotification() {
1198 if (Config.SUPPRESS_ERROR_NOTIFICATION) {
1199 cancel(ERROR_NOTIFICATION_ID);
1200 return;
1201 }
1202 final boolean showAllErrors = QuickConversationsService.isConversations();
1203 final List<Account> errors = new ArrayList<>();
1204 boolean torNotAvailable = false;
1205 for (final Account account : mXmppConnectionService.getAccounts()) {
1206 if (account.hasErrorStatus() && account.showErrorNotification() && (showAllErrors || account.getLastErrorStatus() == Account.State.UNAUTHORIZED)) {
1207 errors.add(account);
1208 torNotAvailable |= account.getStatus() == Account.State.TOR_NOT_AVAILABLE;
1209 }
1210 }
1211 if (mXmppConnectionService.foregroundNotificationNeedsUpdatingWhenErrorStateChanges()) {
1212 notify(FOREGROUND_NOTIFICATION_ID, createForegroundNotification());
1213 }
1214 final Notification.Builder mBuilder = new Notification.Builder(mXmppConnectionService);
1215 if (errors.size() == 0) {
1216 cancel(ERROR_NOTIFICATION_ID);
1217 return;
1218 } else if (errors.size() == 1) {
1219 mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.problem_connecting_to_account));
1220 mBuilder.setContentText(errors.get(0).getJid().asBareJid().toEscapedString());
1221 } else {
1222 mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.problem_connecting_to_accounts));
1223 mBuilder.setContentText(mXmppConnectionService.getString(R.string.touch_to_fix));
1224 }
1225 mBuilder.addAction(R.drawable.ic_autorenew_white_24dp,
1226 mXmppConnectionService.getString(R.string.try_again),
1227 createTryAgainIntent()
1228 );
1229 if (torNotAvailable) {
1230 if (TorServiceUtils.isOrbotInstalled(mXmppConnectionService)) {
1231 mBuilder.addAction(
1232 R.drawable.ic_play_circle_filled_white_48dp,
1233 mXmppConnectionService.getString(R.string.start_orbot),
1234 PendingIntent.getActivity(mXmppConnectionService, 147, TorServiceUtils.LAUNCH_INTENT, 0)
1235 );
1236 } else {
1237 mBuilder.addAction(
1238 R.drawable.ic_file_download_white_24dp,
1239 mXmppConnectionService.getString(R.string.install_orbot),
1240 PendingIntent.getActivity(mXmppConnectionService, 146, TorServiceUtils.INSTALL_INTENT, 0)
1241 );
1242 }
1243 }
1244 mBuilder.setDeleteIntent(createDismissErrorIntent());
1245 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
1246 mBuilder.setVisibility(Notification.VISIBILITY_PRIVATE);
1247 mBuilder.setSmallIcon(R.drawable.ic_warning_white_24dp);
1248 } else {
1249 mBuilder.setSmallIcon(R.drawable.ic_stat_alert_warning);
1250 }
1251 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) {
1252 mBuilder.setLocalOnly(true);
1253 }
1254 mBuilder.setPriority(Notification.PRIORITY_LOW);
1255 final Intent intent;
1256 if (AccountUtils.MANAGE_ACCOUNT_ACTIVITY != null) {
1257 intent = new Intent(mXmppConnectionService, AccountUtils.MANAGE_ACCOUNT_ACTIVITY);
1258 } else {
1259 intent = new Intent(mXmppConnectionService, EditAccountActivity.class);
1260 intent.putExtra("jid", errors.get(0).getJid().asBareJid().toEscapedString());
1261 intent.putExtra(EditAccountActivity.EXTRA_OPENED_FROM_NOTIFICATION, true);
1262 }
1263 mBuilder.setContentIntent(PendingIntent.getActivity(mXmppConnectionService, 145, intent, PendingIntent.FLAG_UPDATE_CURRENT));
1264 if (Compatibility.runsTwentySix()) {
1265 mBuilder.setChannelId("error");
1266 }
1267 notify(ERROR_NOTIFICATION_ID, mBuilder.build());
1268 }
1269
1270 void updateFileAddingNotification(int current, Message message) {
1271 Notification.Builder mBuilder = new Notification.Builder(mXmppConnectionService);
1272 mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.transcoding_video));
1273 mBuilder.setProgress(100, current, false);
1274 mBuilder.setSmallIcon(R.drawable.ic_hourglass_empty_white_24dp);
1275 mBuilder.setContentIntent(createContentIntent(message.getConversation()));
1276 mBuilder.setOngoing(true);
1277 if (Compatibility.runsTwentySix()) {
1278 mBuilder.setChannelId("compression");
1279 }
1280 Notification notification = mBuilder.build();
1281 notify(FOREGROUND_NOTIFICATION_ID, notification);
1282 }
1283
1284 private void notify(String tag, int id, Notification notification) {
1285 final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(mXmppConnectionService);
1286 try {
1287 notificationManager.notify(tag, id, notification);
1288 } catch (RuntimeException e) {
1289 Log.d(Config.LOGTAG, "unable to make notification", e);
1290 }
1291 }
1292
1293 public void notify(int id, Notification notification) {
1294 final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(mXmppConnectionService);
1295 try {
1296 notificationManager.notify(id, notification);
1297 } catch (RuntimeException e) {
1298 Log.d(Config.LOGTAG, "unable to make notification", e);
1299 }
1300 }
1301
1302 public void cancel(int id) {
1303 final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(mXmppConnectionService);
1304 try {
1305 notificationManager.cancel(id);
1306 } catch (RuntimeException e) {
1307 Log.d(Config.LOGTAG, "unable to cancel notification", e);
1308 }
1309 }
1310
1311 private void cancel(String tag, int id) {
1312 final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(mXmppConnectionService);
1313 try {
1314 notificationManager.cancel(tag, id);
1315 } catch (RuntimeException e) {
1316 Log.d(Config.LOGTAG, "unable to cancel notification", e);
1317 }
1318 }
1319
1320 private class VibrationRunnable implements Runnable {
1321
1322 @Override
1323 public void run() {
1324 final Vibrator vibrator = (Vibrator) mXmppConnectionService.getSystemService(Context.VIBRATOR_SERVICE);
1325 vibrator.vibrate(CALL_PATTERN, -1);
1326 }
1327 }
1328}