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