NotificationService.java

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