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