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 clearMissedCalls() {
 777        synchronized (mMissedCalls) {
 778            for (final Conversational conversation : mMissedCalls.keySet()) {
 779                cancel(conversation.getUuid(), MISSED_CALL_NOTIFICATION_ID);
 780            }
 781            mMissedCalls.clear();
 782            updateMissedCallNotifications(null);
 783        }
 784    }
 785
 786    public void clearMissedCalls(final Conversation conversation) {
 787        synchronized (mMissedCalls) {
 788            if (mMissedCalls.remove(conversation) != null) {
 789                cancel(conversation.getUuid(), MISSED_CALL_NOTIFICATION_ID);
 790                updateMissedCallNotifications(null);
 791            }
 792        }
 793    }
 794
 795    private void markAsReadIfHasDirectReply(final Conversation conversation) {
 796        markAsReadIfHasDirectReply(notifications.get(conversation.getUuid()));
 797    }
 798
 799    private void markAsReadIfHasDirectReply(final ArrayList<Message> messages) {
 800        if (messages != null && messages.size() > 0) {
 801            Message last = messages.get(messages.size() - 1);
 802            if (last.getStatus() != Message.STATUS_RECEIVED) {
 803                if (mXmppConnectionService.markRead((Conversation) last.getConversation(), false)) {
 804                    mXmppConnectionService.updateConversationUi();
 805                }
 806            }
 807        }
 808    }
 809
 810    private void setNotificationColor(final Builder mBuilder) {
 811        mBuilder.setColor(ContextCompat.getColor(mXmppConnectionService, R.color.green600));
 812    }
 813
 814    public void updateNotification() {
 815        synchronized (notifications) {
 816            updateNotification(false);
 817        }
 818    }
 819
 820    private void updateNotification(final boolean notify) {
 821        updateNotification(notify, null, false);
 822    }
 823
 824    private void updateNotification(final boolean notify, final List<String> conversations) {
 825        updateNotification(notify, conversations, false);
 826    }
 827
 828    private void updateNotification(
 829            final boolean notify, final List<String> conversations, final boolean summaryOnly) {
 830        final SharedPreferences preferences =
 831                PreferenceManager.getDefaultSharedPreferences(mXmppConnectionService);
 832
 833        final boolean quiteHours = isQuietHours();
 834
 835        final boolean notifyOnlyOneChild =
 836                notify
 837                        && conversations != null
 838                        && conversations.size()
 839                                == 1; // if this check is changed to > 0 catchup messages will
 840        // create one notification per conversation
 841
 842        if (notifications.size() == 0) {
 843            cancel(NOTIFICATION_ID);
 844        } else {
 845            if (notify) {
 846                this.markLastNotification();
 847            }
 848            final Builder mBuilder;
 849            if (notifications.size() == 1 && Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
 850                mBuilder =
 851                        buildSingleConversations(
 852                                notifications.values().iterator().next(), notify, quiteHours);
 853                modifyForSoundVibrationAndLight(mBuilder, notify, quiteHours, preferences);
 854                notify(NOTIFICATION_ID, mBuilder.build());
 855            } else {
 856                mBuilder = buildMultipleConversation(notify, quiteHours);
 857                if (notifyOnlyOneChild) {
 858                    mBuilder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN);
 859                }
 860                modifyForSoundVibrationAndLight(mBuilder, notify, quiteHours, preferences);
 861                if (!summaryOnly) {
 862                    for (Map.Entry<String, ArrayList<Message>> entry : notifications.entrySet()) {
 863                        String uuid = entry.getKey();
 864                        final boolean notifyThis =
 865                                notifyOnlyOneChild ? conversations.contains(uuid) : notify;
 866                        Builder singleBuilder =
 867                                buildSingleConversations(entry.getValue(), notifyThis, quiteHours);
 868                        if (!notifyOnlyOneChild) {
 869                            singleBuilder.setGroupAlertBehavior(
 870                                    NotificationCompat.GROUP_ALERT_SUMMARY);
 871                        }
 872                        modifyForSoundVibrationAndLight(
 873                                singleBuilder, notifyThis, quiteHours, preferences);
 874                        singleBuilder.setGroup(MESSAGES_GROUP);
 875                        setNotificationColor(singleBuilder);
 876                        notify(entry.getKey(), NOTIFICATION_ID, singleBuilder.build());
 877                    }
 878                }
 879                notify(NOTIFICATION_ID, mBuilder.build());
 880            }
 881        }
 882    }
 883
 884    private void updateMissedCallNotifications(final Set<Conversational> update) {
 885        if (mMissedCalls.isEmpty()) {
 886            cancel(MISSED_CALL_NOTIFICATION_ID);
 887            return;
 888        }
 889        if (mMissedCalls.size() == 1 && Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
 890            final Conversational conversation = mMissedCalls.keySet().iterator().next();
 891            final MissedCallsInfo info = mMissedCalls.values().iterator().next();
 892            final Notification notification = missedCall(conversation, info);
 893            notify(MISSED_CALL_NOTIFICATION_ID, notification);
 894        } else {
 895            final Notification summary = missedCallsSummary();
 896            notify(MISSED_CALL_NOTIFICATION_ID, summary);
 897            if (update != null) {
 898                for (final Conversational conversation : update) {
 899                    final MissedCallsInfo info = mMissedCalls.get(conversation);
 900                    if (info != null) {
 901                        final Notification notification = missedCall(conversation, info);
 902                        notify(conversation.getUuid(), MISSED_CALL_NOTIFICATION_ID, notification);
 903                    }
 904                }
 905            }
 906        }
 907    }
 908
 909    private void modifyForSoundVibrationAndLight(
 910            Builder mBuilder, boolean notify, boolean quietHours, SharedPreferences preferences) {
 911        final Resources resources = mXmppConnectionService.getResources();
 912        final String ringtone =
 913                preferences.getString(
 914                        "notification_ringtone",
 915                        resources.getString(R.string.notification_ringtone));
 916        final boolean vibrate =
 917                preferences.getBoolean(
 918                        "vibrate_on_notification",
 919                        resources.getBoolean(R.bool.vibrate_on_notification));
 920        final boolean led = preferences.getBoolean("led", resources.getBoolean(R.bool.led));
 921        final boolean headsup =
 922                preferences.getBoolean(
 923                        "notification_headsup", resources.getBoolean(R.bool.headsup_notifications));
 924        if (notify && !quietHours) {
 925            if (vibrate) {
 926                final int dat = 70;
 927                final long[] pattern = {0, 3 * dat, dat, dat};
 928                mBuilder.setVibrate(pattern);
 929            } else {
 930                mBuilder.setVibrate(new long[] {0});
 931            }
 932            Uri uri = Uri.parse(ringtone);
 933            try {
 934                mBuilder.setSound(fixRingtoneUri(uri));
 935            } catch (SecurityException e) {
 936                Log.d(Config.LOGTAG, "unable to use custom notification sound " + uri.toString());
 937            }
 938        } else {
 939            mBuilder.setLocalOnly(true);
 940        }
 941        mBuilder.setCategory(Notification.CATEGORY_MESSAGE);
 942        mBuilder.setPriority(
 943                notify
 944                        ? (headsup
 945                                ? NotificationCompat.PRIORITY_HIGH
 946                                : NotificationCompat.PRIORITY_DEFAULT)
 947                        : NotificationCompat.PRIORITY_LOW);
 948        setNotificationColor(mBuilder);
 949        mBuilder.setDefaults(0);
 950        if (led) {
 951            mBuilder.setLights(LED_COLOR, 2000, 3000);
 952        }
 953    }
 954
 955    private void modifyIncomingCall(final Builder mBuilder) {
 956        mBuilder.setPriority(NotificationCompat.PRIORITY_HIGH);
 957        setNotificationColor(mBuilder);
 958        mBuilder.setLights(LED_COLOR, 2000, 3000);
 959    }
 960
 961    private Uri fixRingtoneUri(Uri uri) {
 962        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && "file".equals(uri.getScheme())) {
 963            return FileBackend.getUriForFile(mXmppConnectionService, new File(uri.getPath()));
 964        } else {
 965            return uri;
 966        }
 967    }
 968
 969    private Notification missedCallsSummary() {
 970        final Builder publicBuilder = buildMissedCallsSummary(true);
 971        final Builder builder = buildMissedCallsSummary(false);
 972        builder.setPublicVersion(publicBuilder.build());
 973        return builder.build();
 974    }
 975
 976    private Builder buildMissedCallsSummary(boolean publicVersion) {
 977        final Builder builder =
 978                new NotificationCompat.Builder(mXmppConnectionService, "missed_calls");
 979        int totalCalls = 0;
 980        final List<String> names = new ArrayList<>();
 981        long lastTime = 0;
 982        for (final Map.Entry<Conversational, MissedCallsInfo> entry : mMissedCalls.entrySet()) {
 983            final Conversational conversation = entry.getKey();
 984            final MissedCallsInfo missedCallsInfo = entry.getValue();
 985            names.add(conversation.getContact().getDisplayName());
 986            totalCalls += missedCallsInfo.getNumberOfCalls();
 987            lastTime = Math.max(lastTime, missedCallsInfo.getLastTime());
 988        }
 989        final String title =
 990                (totalCalls == 1)
 991                        ? mXmppConnectionService.getString(R.string.missed_call)
 992                        : (mMissedCalls.size() == 1)
 993                                ? mXmppConnectionService
 994                                        .getResources()
 995                                        .getQuantityString(
 996                                                R.plurals.n_missed_calls, totalCalls, totalCalls)
 997                                : mXmppConnectionService
 998                                        .getResources()
 999                                        .getQuantityString(
1000                                                R.plurals.n_missed_calls_from_m_contacts,
1001                                                mMissedCalls.size(),
1002                                                totalCalls,
1003                                                mMissedCalls.size());
1004        builder.setContentTitle(title);
1005        builder.setTicker(title);
1006        if (!publicVersion) {
1007            builder.setContentText(Joiner.on(", ").join(names));
1008        }
1009        builder.setSmallIcon(R.drawable.ic_call_missed_white_24db);
1010        builder.setGroupSummary(true);
1011        builder.setGroup(MISSED_CALLS_GROUP);
1012        builder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN);
1013        builder.setCategory(NotificationCompat.CATEGORY_CALL);
1014        builder.setWhen(lastTime);
1015        if (!mMissedCalls.isEmpty()) {
1016            final Conversational firstConversation = mMissedCalls.keySet().iterator().next();
1017            builder.setContentIntent(createContentIntent(firstConversation));
1018        }
1019        builder.setDeleteIntent(createMissedCallsDeleteIntent(null));
1020        modifyMissedCall(builder);
1021        return builder;
1022    }
1023
1024    private Notification missedCall(final Conversational conversation, final MissedCallsInfo info) {
1025        final Builder publicBuilder = buildMissedCall(conversation, info, true);
1026        final Builder builder = buildMissedCall(conversation, info, false);
1027        builder.setPublicVersion(publicBuilder.build());
1028        return builder.build();
1029    }
1030
1031    private Builder buildMissedCall(
1032            final Conversational conversation, final MissedCallsInfo info, boolean publicVersion) {
1033        final Builder builder =
1034                new NotificationCompat.Builder(mXmppConnectionService, "missed_calls");
1035        final String title =
1036                (info.getNumberOfCalls() == 1)
1037                        ? mXmppConnectionService.getString(R.string.missed_call)
1038                        : mXmppConnectionService
1039                                .getResources()
1040                                .getQuantityString(
1041                                        R.plurals.n_missed_calls,
1042                                        info.getNumberOfCalls(),
1043                                        info.getNumberOfCalls());
1044        builder.setContentTitle(title);
1045        final String name = conversation.getContact().getDisplayName();
1046        if (publicVersion) {
1047            builder.setTicker(title);
1048        } else {
1049            builder.setTicker(
1050                    mXmppConnectionService
1051                            .getResources()
1052                            .getQuantityString(
1053                                    R.plurals.n_missed_calls_from_x,
1054                                    info.getNumberOfCalls(),
1055                                    info.getNumberOfCalls(),
1056                                    name));
1057            builder.setContentText(name);
1058        }
1059        builder.setSmallIcon(R.drawable.ic_call_missed_white_24db);
1060        builder.setGroup(MISSED_CALLS_GROUP);
1061        builder.setCategory(NotificationCompat.CATEGORY_CALL);
1062        builder.setWhen(info.getLastTime());
1063        builder.setContentIntent(createContentIntent(conversation));
1064        builder.setDeleteIntent(createMissedCallsDeleteIntent(conversation));
1065        if (!publicVersion && conversation instanceof Conversation) {
1066            builder.setLargeIcon(
1067                    mXmppConnectionService
1068                            .getAvatarService()
1069                            .get(
1070                                    (Conversation) conversation,
1071                                    AvatarService.getSystemUiAvatarSize(mXmppConnectionService)));
1072        }
1073        modifyMissedCall(builder);
1074        return builder;
1075    }
1076
1077    private void modifyMissedCall(final Builder builder) {
1078        final SharedPreferences preferences =
1079                PreferenceManager.getDefaultSharedPreferences(mXmppConnectionService);
1080        final Resources resources = mXmppConnectionService.getResources();
1081        final boolean led = preferences.getBoolean("led", resources.getBoolean(R.bool.led));
1082        if (led) {
1083            builder.setLights(LED_COLOR, 2000, 3000);
1084        }
1085        builder.setPriority(NotificationCompat.PRIORITY_HIGH);
1086        builder.setSound(null);
1087        setNotificationColor(builder);
1088    }
1089
1090    private Builder buildMultipleConversation(final boolean notify, final boolean quietHours) {
1091        final Builder mBuilder =
1092                new NotificationCompat.Builder(
1093                        mXmppConnectionService,
1094                        quietHours ? "quiet_hours" : (notify ? "messages" : "silent_messages"));
1095        final NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle();
1096        style.setBigContentTitle(
1097                mXmppConnectionService
1098                        .getResources()
1099                        .getQuantityString(
1100                                R.plurals.x_unread_conversations,
1101                                notifications.size(),
1102                                notifications.size()));
1103        final List<String> names = new ArrayList<>();
1104        Conversation conversation = null;
1105        for (final ArrayList<Message> messages : notifications.values()) {
1106            if (messages.isEmpty()) {
1107                continue;
1108            }
1109            conversation = (Conversation) messages.get(0).getConversation();
1110            final String name = conversation.getName().toString();
1111            SpannableString styledString;
1112            if (Config.HIDE_MESSAGE_TEXT_IN_NOTIFICATION) {
1113                int count = messages.size();
1114                styledString =
1115                        new SpannableString(
1116                                name
1117                                        + ": "
1118                                        + mXmppConnectionService
1119                                                .getResources()
1120                                                .getQuantityString(
1121                                                        R.plurals.x_messages, count, count));
1122                styledString.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), 0);
1123                style.addLine(styledString);
1124            } else {
1125                styledString =
1126                        new SpannableString(
1127                                name
1128                                        + ": "
1129                                        + UIHelper.getMessagePreview(
1130                                                        mXmppConnectionService, messages.get(0))
1131                                                .first);
1132                styledString.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), 0);
1133                style.addLine(styledString);
1134            }
1135            names.add(name);
1136        }
1137        final String contentTitle =
1138                mXmppConnectionService
1139                        .getResources()
1140                        .getQuantityString(
1141                                R.plurals.x_unread_conversations,
1142                                notifications.size(),
1143                                notifications.size());
1144        mBuilder.setContentTitle(contentTitle);
1145        mBuilder.setTicker(contentTitle);
1146        mBuilder.setContentText(Joiner.on(", ").join(names));
1147        mBuilder.setStyle(style);
1148        if (conversation != null) {
1149            mBuilder.setContentIntent(createContentIntent(conversation));
1150        }
1151        mBuilder.setGroupSummary(true);
1152        mBuilder.setGroup(MESSAGES_GROUP);
1153        mBuilder.setDeleteIntent(createDeleteIntent(null));
1154        mBuilder.setSmallIcon(R.drawable.ic_notification);
1155        return mBuilder;
1156    }
1157
1158    private Builder buildSingleConversations(
1159            final ArrayList<Message> messages, final boolean notify, final boolean quietHours) {
1160        final Builder mBuilder =
1161                new NotificationCompat.Builder(
1162                        mXmppConnectionService,
1163                        quietHours ? "quiet_hours" : (notify ? "messages" : "silent_messages"));
1164        if (messages.size() >= 1) {
1165            final Conversation conversation = (Conversation) messages.get(0).getConversation();
1166            mBuilder.setLargeIcon(
1167                    mXmppConnectionService
1168                            .getAvatarService()
1169                            .get(
1170                                    conversation,
1171                                    AvatarService.getSystemUiAvatarSize(mXmppConnectionService)));
1172            mBuilder.setContentTitle(conversation.getName());
1173            if (Config.HIDE_MESSAGE_TEXT_IN_NOTIFICATION) {
1174                int count = messages.size();
1175                mBuilder.setContentText(
1176                        mXmppConnectionService
1177                                .getResources()
1178                                .getQuantityString(R.plurals.x_messages, count, count));
1179            } else {
1180                Message message;
1181                // TODO starting with Android 9 we might want to put images in MessageStyle
1182                if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P
1183                        && (message = getImage(messages)) != null) {
1184                    modifyForImage(mBuilder, message, messages);
1185                } else {
1186                    modifyForTextOnly(mBuilder, messages);
1187                }
1188                RemoteInput remoteInput =
1189                        new RemoteInput.Builder("text_reply")
1190                                .setLabel(
1191                                        UIHelper.getMessageHint(
1192                                                mXmppConnectionService, conversation))
1193                                .build();
1194                PendingIntent markAsReadPendingIntent = createReadPendingIntent(conversation);
1195                NotificationCompat.Action markReadAction =
1196                        new NotificationCompat.Action.Builder(
1197                                        R.drawable.ic_drafts_white_24dp,
1198                                        mXmppConnectionService.getString(R.string.mark_as_read),
1199                                        markAsReadPendingIntent)
1200                                .setSemanticAction(
1201                                        NotificationCompat.Action.SEMANTIC_ACTION_MARK_AS_READ)
1202                                .setShowsUserInterface(false)
1203                                .build();
1204                final String replyLabel = mXmppConnectionService.getString(R.string.reply);
1205                final String lastMessageUuid = Iterables.getLast(messages).getUuid();
1206                final NotificationCompat.Action replyAction =
1207                        new NotificationCompat.Action.Builder(
1208                                        R.drawable.ic_send_text_offline,
1209                                        replyLabel,
1210                                        createReplyIntent(conversation, lastMessageUuid, false))
1211                                .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY)
1212                                .setShowsUserInterface(false)
1213                                .addRemoteInput(remoteInput)
1214                                .build();
1215                final NotificationCompat.Action wearReplyAction =
1216                        new NotificationCompat.Action.Builder(
1217                                        R.drawable.ic_wear_reply,
1218                                        replyLabel,
1219                                        createReplyIntent(conversation, lastMessageUuid, true))
1220                                .addRemoteInput(remoteInput)
1221                                .build();
1222                mBuilder.extend(
1223                        new NotificationCompat.WearableExtender().addAction(wearReplyAction));
1224                int addedActionsCount = 1;
1225                mBuilder.addAction(markReadAction);
1226                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
1227                    mBuilder.addAction(replyAction);
1228                    ++addedActionsCount;
1229                }
1230
1231                if (displaySnoozeAction(messages)) {
1232                    String label = mXmppConnectionService.getString(R.string.snooze);
1233                    PendingIntent pendingSnoozeIntent = createSnoozeIntent(conversation);
1234                    NotificationCompat.Action snoozeAction =
1235                            new NotificationCompat.Action.Builder(
1236                                            R.drawable.ic_notifications_paused_white_24dp,
1237                                            label,
1238                                            pendingSnoozeIntent)
1239                                    .build();
1240                    mBuilder.addAction(snoozeAction);
1241                    ++addedActionsCount;
1242                }
1243                if (addedActionsCount < 3) {
1244                    final Message firstLocationMessage = getFirstLocationMessage(messages);
1245                    if (firstLocationMessage != null) {
1246                        final PendingIntent pendingShowLocationIntent =
1247                                createShowLocationIntent(firstLocationMessage);
1248                        if (pendingShowLocationIntent != null) {
1249                            final String label =
1250                                    mXmppConnectionService
1251                                            .getResources()
1252                                            .getString(R.string.show_location);
1253                            NotificationCompat.Action locationAction =
1254                                    new NotificationCompat.Action.Builder(
1255                                                    R.drawable.ic_room_white_24dp,
1256                                                    label,
1257                                                    pendingShowLocationIntent)
1258                                            .build();
1259                            mBuilder.addAction(locationAction);
1260                            ++addedActionsCount;
1261                        }
1262                    }
1263                }
1264                if (addedActionsCount < 3) {
1265                    Message firstDownloadableMessage = getFirstDownloadableMessage(messages);
1266                    if (firstDownloadableMessage != null) {
1267                        String label =
1268                                mXmppConnectionService
1269                                        .getResources()
1270                                        .getString(
1271                                                R.string.download_x_file,
1272                                                UIHelper.getFileDescriptionString(
1273                                                        mXmppConnectionService,
1274                                                        firstDownloadableMessage));
1275                        PendingIntent pendingDownloadIntent =
1276                                createDownloadIntent(firstDownloadableMessage);
1277                        NotificationCompat.Action downloadAction =
1278                                new NotificationCompat.Action.Builder(
1279                                                R.drawable.ic_file_download_white_24dp,
1280                                                label,
1281                                                pendingDownloadIntent)
1282                                        .build();
1283                        mBuilder.addAction(downloadAction);
1284                        ++addedActionsCount;
1285                    }
1286                }
1287            }
1288            final ShortcutInfoCompat info;
1289            if (conversation.getMode() == Conversation.MODE_SINGLE) {
1290                final Contact contact = conversation.getContact();
1291                final Uri systemAccount = contact.getSystemAccount();
1292                if (systemAccount != null) {
1293                    mBuilder.addPerson(systemAccount.toString());
1294                }
1295                info = mXmppConnectionService.getShortcutService().getShortcutInfoCompat(contact);
1296            } else {
1297                info =
1298                        mXmppConnectionService
1299                                .getShortcutService()
1300                                .getShortcutInfoCompat(conversation.getMucOptions());
1301            }
1302            mBuilder.setWhen(conversation.getLatestMessage().getTimeSent());
1303            mBuilder.setSmallIcon(R.drawable.ic_notification);
1304            mBuilder.setDeleteIntent(createDeleteIntent(conversation));
1305            mBuilder.setContentIntent(createContentIntent(conversation));
1306            mBuilder.setShortcutInfo(info);
1307            if (Build.VERSION.SDK_INT >= 30) {
1308                mXmppConnectionService
1309                        .getSystemService(ShortcutManager.class)
1310                        .pushDynamicShortcut(info.toShortcutInfo());
1311            }
1312        }
1313        return mBuilder;
1314    }
1315
1316    private void modifyForImage(
1317            final Builder builder, final Message message, final ArrayList<Message> messages) {
1318        try {
1319            final Bitmap bitmap =
1320                    mXmppConnectionService
1321                            .getFileBackend()
1322                            .getThumbnail(message, getPixel(288), false);
1323            final ArrayList<Message> tmp = new ArrayList<>();
1324            for (final Message msg : messages) {
1325                if (msg.getType() == Message.TYPE_TEXT && msg.getTransferable() == null) {
1326                    tmp.add(msg);
1327                }
1328            }
1329            final BigPictureStyle bigPictureStyle = new NotificationCompat.BigPictureStyle();
1330            bigPictureStyle.bigPicture(bitmap);
1331            if (tmp.size() > 0) {
1332                CharSequence text = getMergedBodies(tmp);
1333                bigPictureStyle.setSummaryText(text);
1334                builder.setContentText(text);
1335                builder.setTicker(text);
1336            } else {
1337                final String description =
1338                        UIHelper.getFileDescriptionString(mXmppConnectionService, message);
1339                builder.setContentText(description);
1340                builder.setTicker(description);
1341            }
1342            builder.setStyle(bigPictureStyle);
1343        } catch (final IOException e) {
1344            modifyForTextOnly(builder, messages);
1345        }
1346    }
1347
1348    private Person getPerson(Message message) {
1349        final Contact contact = message.getContact();
1350        final Person.Builder builder = new Person.Builder();
1351        if (contact != null) {
1352            builder.setName(contact.getDisplayName());
1353            final Uri uri = contact.getSystemAccount();
1354            if (uri != null) {
1355                builder.setUri(uri.toString());
1356            }
1357        } else {
1358            builder.setName(UIHelper.getMessageDisplayName(message));
1359        }
1360        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
1361            builder.setIcon(
1362                    IconCompat.createWithBitmap(
1363                            mXmppConnectionService
1364                                    .getAvatarService()
1365                                    .get(
1366                                            message,
1367                                            AvatarService.getSystemUiAvatarSize(
1368                                                    mXmppConnectionService),
1369                                            false)));
1370        }
1371        return builder.build();
1372    }
1373
1374    private void modifyForTextOnly(final Builder builder, final ArrayList<Message> messages) {
1375        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
1376            final Conversation conversation = (Conversation) messages.get(0).getConversation();
1377            final Person.Builder meBuilder =
1378                    new Person.Builder().setName(mXmppConnectionService.getString(R.string.me));
1379            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
1380                meBuilder.setIcon(
1381                        IconCompat.createWithBitmap(
1382                                mXmppConnectionService
1383                                        .getAvatarService()
1384                                        .get(
1385                                                conversation.getAccount(),
1386                                                AvatarService.getSystemUiAvatarSize(
1387                                                        mXmppConnectionService))));
1388            }
1389            final Person me = meBuilder.build();
1390            NotificationCompat.MessagingStyle messagingStyle =
1391                    new NotificationCompat.MessagingStyle(me);
1392            final boolean multiple = conversation.getMode() == Conversation.MODE_MULTI;
1393            if (multiple) {
1394                messagingStyle.setConversationTitle(conversation.getName());
1395            }
1396            for (Message message : messages) {
1397                final Person sender =
1398                        message.getStatus() == Message.STATUS_RECEIVED ? getPerson(message) : null;
1399                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && isImageMessage(message)) {
1400                    final Uri dataUri =
1401                            FileBackend.getMediaUri(
1402                                    mXmppConnectionService,
1403                                    mXmppConnectionService.getFileBackend().getFile(message));
1404                    NotificationCompat.MessagingStyle.Message imageMessage =
1405                            new NotificationCompat.MessagingStyle.Message(
1406                                    UIHelper.getMessagePreview(mXmppConnectionService, message)
1407                                            .first,
1408                                    message.getTimeSent(),
1409                                    sender);
1410                    if (dataUri != null) {
1411                        imageMessage.setData(message.getMimeType(), dataUri);
1412                    }
1413                    messagingStyle.addMessage(imageMessage);
1414                } else {
1415                    messagingStyle.addMessage(
1416                            UIHelper.getMessagePreview(mXmppConnectionService, message).first,
1417                            message.getTimeSent(),
1418                            sender);
1419                }
1420            }
1421            messagingStyle.setGroupConversation(multiple);
1422            builder.setStyle(messagingStyle);
1423        } else {
1424            if (messages.get(0).getConversation().getMode() == Conversation.MODE_SINGLE) {
1425                builder.setStyle(
1426                        new NotificationCompat.BigTextStyle().bigText(getMergedBodies(messages)));
1427                final CharSequence preview =
1428                        UIHelper.getMessagePreview(
1429                                        mXmppConnectionService, messages.get(messages.size() - 1))
1430                                .first;
1431                builder.setContentText(preview);
1432                builder.setTicker(preview);
1433                builder.setNumber(messages.size());
1434            } else {
1435                final NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle();
1436                SpannableString styledString;
1437                for (Message message : messages) {
1438                    final String name = UIHelper.getMessageDisplayName(message);
1439                    styledString = new SpannableString(name + ": " + message.getBody());
1440                    styledString.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), 0);
1441                    style.addLine(styledString);
1442                }
1443                builder.setStyle(style);
1444                int count = messages.size();
1445                if (count == 1) {
1446                    final String name = UIHelper.getMessageDisplayName(messages.get(0));
1447                    styledString = new SpannableString(name + ": " + messages.get(0).getBody());
1448                    styledString.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), 0);
1449                    builder.setContentText(styledString);
1450                    builder.setTicker(styledString);
1451                } else {
1452                    final String text =
1453                            mXmppConnectionService
1454                                    .getResources()
1455                                    .getQuantityString(R.plurals.x_messages, count, count);
1456                    builder.setContentText(text);
1457                    builder.setTicker(text);
1458                }
1459            }
1460        }
1461    }
1462
1463    private Message getImage(final Iterable<Message> messages) {
1464        Message image = null;
1465        for (final Message message : messages) {
1466            if (message.getStatus() != Message.STATUS_RECEIVED) {
1467                return null;
1468            }
1469            if (isImageMessage(message)) {
1470                image = message;
1471            }
1472        }
1473        return image;
1474    }
1475
1476    private Message getFirstDownloadableMessage(final Iterable<Message> messages) {
1477        for (final Message message : messages) {
1478            if (message.getTransferable() != null
1479                    || (message.getType() == Message.TYPE_TEXT && message.treatAsDownloadable())) {
1480                return message;
1481            }
1482        }
1483        return null;
1484    }
1485
1486    private Message getFirstLocationMessage(final Iterable<Message> messages) {
1487        for (final Message message : messages) {
1488            if (message.isGeoUri()) {
1489                return message;
1490            }
1491        }
1492        return null;
1493    }
1494
1495    private CharSequence getMergedBodies(final ArrayList<Message> messages) {
1496        final StringBuilder text = new StringBuilder();
1497        for (Message message : messages) {
1498            if (text.length() != 0) {
1499                text.append("\n");
1500            }
1501            text.append(UIHelper.getMessagePreview(mXmppConnectionService, message).first);
1502        }
1503        return text.toString();
1504    }
1505
1506    private PendingIntent createShowLocationIntent(final Message message) {
1507        Iterable<Intent> intents =
1508                GeoHelper.createGeoIntentsFromMessage(mXmppConnectionService, message);
1509        for (final Intent intent : intents) {
1510            if (intent.resolveActivity(mXmppConnectionService.getPackageManager()) != null) {
1511                return PendingIntent.getActivity(
1512                        mXmppConnectionService,
1513                        generateRequestCode(message.getConversation(), 18),
1514                        intent,
1515                        s()
1516                                ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1517                                : PendingIntent.FLAG_UPDATE_CURRENT);
1518            }
1519        }
1520        return null;
1521    }
1522
1523    private PendingIntent createContentIntent(
1524            final String conversationUuid, final String downloadMessageUuid) {
1525        final Intent viewConversationIntent =
1526                new Intent(mXmppConnectionService, ConversationsActivity.class);
1527        viewConversationIntent.setAction(ConversationsActivity.ACTION_VIEW_CONVERSATION);
1528        viewConversationIntent.putExtra(ConversationsActivity.EXTRA_CONVERSATION, conversationUuid);
1529        if (downloadMessageUuid != null) {
1530            viewConversationIntent.putExtra(
1531                    ConversationsActivity.EXTRA_DOWNLOAD_UUID, downloadMessageUuid);
1532            return PendingIntent.getActivity(
1533                    mXmppConnectionService,
1534                    generateRequestCode(conversationUuid, 8),
1535                    viewConversationIntent,
1536                    s()
1537                            ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1538                            : PendingIntent.FLAG_UPDATE_CURRENT);
1539        } else {
1540            return PendingIntent.getActivity(
1541                    mXmppConnectionService,
1542                    generateRequestCode(conversationUuid, 10),
1543                    viewConversationIntent,
1544                    s()
1545                            ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1546                            : PendingIntent.FLAG_UPDATE_CURRENT);
1547        }
1548    }
1549
1550    private int generateRequestCode(String uuid, int actionId) {
1551        return (actionId * NOTIFICATION_ID_MULTIPLIER)
1552                + (uuid.hashCode() % NOTIFICATION_ID_MULTIPLIER);
1553    }
1554
1555    private int generateRequestCode(Conversational conversation, int actionId) {
1556        return generateRequestCode(conversation.getUuid(), actionId);
1557    }
1558
1559    private PendingIntent createDownloadIntent(final Message message) {
1560        return createContentIntent(message.getConversationUuid(), message.getUuid());
1561    }
1562
1563    private PendingIntent createContentIntent(final Conversational conversation) {
1564        return createContentIntent(conversation.getUuid(), null);
1565    }
1566
1567    private PendingIntent createDeleteIntent(final Conversation conversation) {
1568        final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
1569        intent.setAction(XmppConnectionService.ACTION_CLEAR_MESSAGE_NOTIFICATION);
1570        if (conversation != null) {
1571            intent.putExtra("uuid", conversation.getUuid());
1572            return PendingIntent.getService(
1573                    mXmppConnectionService,
1574                    generateRequestCode(conversation, 20),
1575                    intent,
1576                    s()
1577                            ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1578                            : PendingIntent.FLAG_UPDATE_CURRENT);
1579        }
1580        return PendingIntent.getService(
1581                mXmppConnectionService,
1582                0,
1583                intent,
1584                s()
1585                        ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1586                        : PendingIntent.FLAG_UPDATE_CURRENT);
1587    }
1588
1589    private PendingIntent createMissedCallsDeleteIntent(final Conversational conversation) {
1590        final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
1591        intent.setAction(XmppConnectionService.ACTION_CLEAR_MISSED_CALL_NOTIFICATION);
1592        if (conversation != null) {
1593            intent.putExtra("uuid", conversation.getUuid());
1594            return PendingIntent.getService(
1595                    mXmppConnectionService,
1596                    generateRequestCode(conversation, 21),
1597                    intent,
1598                    s()
1599                            ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1600                            : PendingIntent.FLAG_UPDATE_CURRENT);
1601        }
1602        return PendingIntent.getService(
1603                mXmppConnectionService,
1604                1,
1605                intent,
1606                s()
1607                        ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1608                        : PendingIntent.FLAG_UPDATE_CURRENT);
1609    }
1610
1611    private PendingIntent createReplyIntent(
1612            final Conversation conversation,
1613            final String lastMessageUuid,
1614            final boolean dismissAfterReply) {
1615        final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
1616        intent.setAction(XmppConnectionService.ACTION_REPLY_TO_CONVERSATION);
1617        intent.putExtra("uuid", conversation.getUuid());
1618        intent.putExtra("dismiss_notification", dismissAfterReply);
1619        intent.putExtra("last_message_uuid", lastMessageUuid);
1620        final int id = generateRequestCode(conversation, dismissAfterReply ? 12 : 14);
1621        return PendingIntent.getService(
1622                mXmppConnectionService,
1623                id,
1624                intent,
1625                s()
1626                        ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1627                        : PendingIntent.FLAG_UPDATE_CURRENT);
1628    }
1629
1630    private PendingIntent createReadPendingIntent(Conversation conversation) {
1631        final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
1632        intent.setAction(XmppConnectionService.ACTION_MARK_AS_READ);
1633        intent.putExtra("uuid", conversation.getUuid());
1634        intent.setPackage(mXmppConnectionService.getPackageName());
1635        return PendingIntent.getService(
1636                mXmppConnectionService,
1637                generateRequestCode(conversation, 16),
1638                intent,
1639                s()
1640                        ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1641                        : PendingIntent.FLAG_UPDATE_CURRENT);
1642    }
1643
1644    private PendingIntent createCallAction(String sessionId, final String action, int requestCode) {
1645        final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
1646        intent.setAction(action);
1647        intent.setPackage(mXmppConnectionService.getPackageName());
1648        intent.putExtra(RtpSessionActivity.EXTRA_SESSION_ID, sessionId);
1649        return PendingIntent.getService(
1650                mXmppConnectionService,
1651                requestCode,
1652                intent,
1653                s()
1654                        ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1655                        : PendingIntent.FLAG_UPDATE_CURRENT);
1656    }
1657
1658    private PendingIntent createSnoozeIntent(Conversation conversation) {
1659        final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
1660        intent.setAction(XmppConnectionService.ACTION_SNOOZE);
1661        intent.putExtra("uuid", conversation.getUuid());
1662        intent.setPackage(mXmppConnectionService.getPackageName());
1663        return PendingIntent.getService(
1664                mXmppConnectionService,
1665                generateRequestCode(conversation, 22),
1666                intent,
1667                s()
1668                        ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1669                        : PendingIntent.FLAG_UPDATE_CURRENT);
1670    }
1671
1672    private PendingIntent createTryAgainIntent() {
1673        final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
1674        intent.setAction(XmppConnectionService.ACTION_TRY_AGAIN);
1675        return PendingIntent.getService(
1676                mXmppConnectionService,
1677                45,
1678                intent,
1679                s()
1680                        ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1681                        : PendingIntent.FLAG_UPDATE_CURRENT);
1682    }
1683
1684    private PendingIntent createDismissErrorIntent() {
1685        final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
1686        intent.setAction(XmppConnectionService.ACTION_DISMISS_ERROR_NOTIFICATIONS);
1687        return PendingIntent.getService(
1688                mXmppConnectionService,
1689                69,
1690                intent,
1691                s()
1692                        ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1693                        : PendingIntent.FLAG_UPDATE_CURRENT);
1694    }
1695
1696    private boolean wasHighlightedOrPrivate(final Message message) {
1697        if (message.getConversation() instanceof Conversation) {
1698            Conversation conversation = (Conversation) message.getConversation();
1699            final String nick = conversation.getMucOptions().getActualNick();
1700            final Pattern highlight = generateNickHighlightPattern(nick);
1701            if (message.getBody() == null || nick == null) {
1702                return false;
1703            }
1704            final Matcher m = highlight.matcher(message.getBody());
1705            return (m.find() || message.isPrivateMessage());
1706        } else {
1707            return false;
1708        }
1709    }
1710
1711    public void setOpenConversation(final Conversation conversation) {
1712        this.mOpenConversation = conversation;
1713    }
1714
1715    public void setIsInForeground(final boolean foreground) {
1716        this.mIsInForeground = foreground;
1717    }
1718
1719    private int getPixel(final int dp) {
1720        final DisplayMetrics metrics = mXmppConnectionService.getResources().getDisplayMetrics();
1721        return ((int) (dp * metrics.density));
1722    }
1723
1724    private void markLastNotification() {
1725        this.mLastNotification = SystemClock.elapsedRealtime();
1726    }
1727
1728    private boolean inMiniGracePeriod(final Account account) {
1729        final int miniGrace =
1730                account.getStatus() == Account.State.ONLINE
1731                        ? Config.MINI_GRACE_PERIOD
1732                        : Config.MINI_GRACE_PERIOD * 2;
1733        return SystemClock.elapsedRealtime() < (this.mLastNotification + miniGrace);
1734    }
1735
1736    Notification createForegroundNotification() {
1737        final Notification.Builder mBuilder = new Notification.Builder(mXmppConnectionService);
1738        mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.app_name));
1739        final List<Account> accounts = mXmppConnectionService.getAccounts();
1740        int enabled = 0;
1741        int connected = 0;
1742        if (accounts != null) {
1743            for (Account account : accounts) {
1744                if (account.isOnlineAndConnected()) {
1745                    connected++;
1746                    enabled++;
1747                } else if (account.isEnabled()) {
1748                    enabled++;
1749                }
1750            }
1751        }
1752        mBuilder.setContentText(
1753                mXmppConnectionService.getString(R.string.connected_accounts, connected, enabled));
1754        final PendingIntent openIntent = createOpenConversationsIntent();
1755        if (openIntent != null) {
1756            mBuilder.setContentIntent(openIntent);
1757        }
1758        mBuilder.setWhen(0)
1759                .setPriority(Notification.PRIORITY_MIN)
1760                .setSmallIcon(
1761                        connected > 0
1762                                ? R.drawable.ic_link_white_24dp
1763                                : R.drawable.ic_link_off_white_24dp)
1764                .setLocalOnly(true);
1765
1766        if (Compatibility.runsTwentySix()) {
1767            mBuilder.setChannelId("foreground");
1768        }
1769
1770        return mBuilder.build();
1771    }
1772
1773    private PendingIntent createOpenConversationsIntent() {
1774        try {
1775            return PendingIntent.getActivity(
1776                    mXmppConnectionService,
1777                    0,
1778                    new Intent(mXmppConnectionService, ConversationsActivity.class),
1779                    s()
1780                            ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1781                            : PendingIntent.FLAG_UPDATE_CURRENT);
1782        } catch (RuntimeException e) {
1783            return null;
1784        }
1785    }
1786
1787    void updateErrorNotification() {
1788        if (Config.SUPPRESS_ERROR_NOTIFICATION) {
1789            cancel(ERROR_NOTIFICATION_ID);
1790            return;
1791        }
1792        final boolean showAllErrors = QuickConversationsService.isConversations();
1793        final List<Account> errors = new ArrayList<>();
1794        boolean torNotAvailable = false;
1795        for (final Account account : mXmppConnectionService.getAccounts()) {
1796            if (account.hasErrorStatus()
1797                    && account.showErrorNotification()
1798                    && (showAllErrors
1799                            || account.getLastErrorStatus() == Account.State.UNAUTHORIZED)) {
1800                errors.add(account);
1801                torNotAvailable |= account.getStatus() == Account.State.TOR_NOT_AVAILABLE;
1802            }
1803        }
1804        if (mXmppConnectionService.foregroundNotificationNeedsUpdatingWhenErrorStateChanges()) {
1805            notify(FOREGROUND_NOTIFICATION_ID, createForegroundNotification());
1806        }
1807        final Notification.Builder mBuilder = new Notification.Builder(mXmppConnectionService);
1808        if (errors.size() == 0) {
1809            cancel(ERROR_NOTIFICATION_ID);
1810            return;
1811        } else if (errors.size() == 1) {
1812            mBuilder.setContentTitle(
1813                    mXmppConnectionService.getString(R.string.problem_connecting_to_account));
1814            mBuilder.setContentText(errors.get(0).getJid().asBareJid().toEscapedString());
1815        } else {
1816            mBuilder.setContentTitle(
1817                    mXmppConnectionService.getString(R.string.problem_connecting_to_accounts));
1818            mBuilder.setContentText(mXmppConnectionService.getString(R.string.touch_to_fix));
1819        }
1820        mBuilder.addAction(
1821                R.drawable.ic_autorenew_white_24dp,
1822                mXmppConnectionService.getString(R.string.try_again),
1823                createTryAgainIntent());
1824        if (torNotAvailable) {
1825            if (TorServiceUtils.isOrbotInstalled(mXmppConnectionService)) {
1826                mBuilder.addAction(
1827                        R.drawable.ic_play_circle_filled_white_48dp,
1828                        mXmppConnectionService.getString(R.string.start_orbot),
1829                        PendingIntent.getActivity(
1830                                mXmppConnectionService,
1831                                147,
1832                                TorServiceUtils.LAUNCH_INTENT,
1833                                s()
1834                                        ? PendingIntent.FLAG_IMMUTABLE
1835                                                | PendingIntent.FLAG_UPDATE_CURRENT
1836                                        : PendingIntent.FLAG_UPDATE_CURRENT));
1837            } else {
1838                mBuilder.addAction(
1839                        R.drawable.ic_file_download_white_24dp,
1840                        mXmppConnectionService.getString(R.string.install_orbot),
1841                        PendingIntent.getActivity(
1842                                mXmppConnectionService,
1843                                146,
1844                                TorServiceUtils.INSTALL_INTENT,
1845                                s()
1846                                        ? PendingIntent.FLAG_IMMUTABLE
1847                                                | PendingIntent.FLAG_UPDATE_CURRENT
1848                                        : PendingIntent.FLAG_UPDATE_CURRENT));
1849            }
1850        }
1851        mBuilder.setDeleteIntent(createDismissErrorIntent());
1852        mBuilder.setVisibility(Notification.VISIBILITY_PRIVATE);
1853        mBuilder.setSmallIcon(R.drawable.ic_warning_white_24dp);
1854        mBuilder.setLocalOnly(true);
1855        mBuilder.setPriority(Notification.PRIORITY_LOW);
1856        final Intent intent;
1857        if (AccountUtils.MANAGE_ACCOUNT_ACTIVITY != null) {
1858            intent = new Intent(mXmppConnectionService, AccountUtils.MANAGE_ACCOUNT_ACTIVITY);
1859        } else {
1860            intent = new Intent(mXmppConnectionService, EditAccountActivity.class);
1861            intent.putExtra("jid", errors.get(0).getJid().asBareJid().toEscapedString());
1862            intent.putExtra(EditAccountActivity.EXTRA_OPENED_FROM_NOTIFICATION, true);
1863        }
1864        mBuilder.setContentIntent(
1865                PendingIntent.getActivity(
1866                        mXmppConnectionService,
1867                        145,
1868                        intent,
1869                        s()
1870                                ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1871                                : PendingIntent.FLAG_UPDATE_CURRENT));
1872        if (Compatibility.runsTwentySix()) {
1873            mBuilder.setChannelId("error");
1874        }
1875        notify(ERROR_NOTIFICATION_ID, mBuilder.build());
1876    }
1877
1878    void updateFileAddingNotification(int current, Message message) {
1879        Notification.Builder mBuilder = new Notification.Builder(mXmppConnectionService);
1880        mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.transcoding_video));
1881        mBuilder.setProgress(100, current, false);
1882        mBuilder.setSmallIcon(R.drawable.ic_hourglass_empty_white_24dp);
1883        mBuilder.setContentIntent(createContentIntent(message.getConversation()));
1884        mBuilder.setOngoing(true);
1885        if (Compatibility.runsTwentySix()) {
1886            mBuilder.setChannelId("compression");
1887        }
1888        Notification notification = mBuilder.build();
1889        notify(FOREGROUND_NOTIFICATION_ID, notification);
1890    }
1891
1892    private void notify(String tag, int id, Notification notification) {
1893        final NotificationManagerCompat notificationManager =
1894                NotificationManagerCompat.from(mXmppConnectionService);
1895        try {
1896            notificationManager.notify(tag, id, notification);
1897        } catch (RuntimeException e) {
1898            Log.d(Config.LOGTAG, "unable to make notification", e);
1899        }
1900    }
1901
1902    public void notify(int id, Notification notification) {
1903        final NotificationManagerCompat notificationManager =
1904                NotificationManagerCompat.from(mXmppConnectionService);
1905        try {
1906            notificationManager.notify(id, notification);
1907        } catch (RuntimeException e) {
1908            Log.d(Config.LOGTAG, "unable to make notification", e);
1909        }
1910    }
1911
1912    public void cancel(int id) {
1913        final NotificationManagerCompat notificationManager =
1914                NotificationManagerCompat.from(mXmppConnectionService);
1915        try {
1916            notificationManager.cancel(id);
1917        } catch (RuntimeException e) {
1918            Log.d(Config.LOGTAG, "unable to cancel notification", e);
1919        }
1920    }
1921
1922    private void cancel(String tag, int id) {
1923        final NotificationManagerCompat notificationManager =
1924                NotificationManagerCompat.from(mXmppConnectionService);
1925        try {
1926            notificationManager.cancel(tag, id);
1927        } catch (RuntimeException e) {
1928            Log.d(Config.LOGTAG, "unable to cancel notification", e);
1929        }
1930    }
1931
1932    private static class MissedCallsInfo {
1933        private int numberOfCalls;
1934        private long lastTime;
1935
1936        MissedCallsInfo(final long time) {
1937            numberOfCalls = 1;
1938            lastTime = time;
1939        }
1940
1941        public void newMissedCall(final long time) {
1942            ++numberOfCalls;
1943            lastTime = time;
1944        }
1945
1946        public int getNumberOfCalls() {
1947            return numberOfCalls;
1948        }
1949
1950        public long getLastTime() {
1951            return lastTime;
1952        }
1953    }
1954
1955    private class VibrationRunnable implements Runnable {
1956
1957        @Override
1958        public void run() {
1959            final Vibrator vibrator =
1960                    (Vibrator) mXmppConnectionService.getSystemService(Context.VIBRATOR_SERVICE);
1961            vibrator.vibrate(CALL_PATTERN, -1);
1962        }
1963    }
1964}