NotificationService.java

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