NotificationService.java

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