NotificationService.java

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