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