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