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