NotificationService.java

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