NotificationService.java

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