NotificationService.java

   1package eu.siacs.conversations.services;
   2
   3import android.app.Notification;
   4import android.app.NotificationChannel;
   5import android.app.NotificationChannelGroup;
   6import android.app.NotificationManager;
   7import android.app.PendingIntent;
   8import android.content.Context;
   9import android.content.Intent;
  10import android.content.SharedPreferences;
  11import android.content.res.Resources;
  12import android.graphics.Bitmap;
  13import android.graphics.Typeface;
  14import android.media.AudioAttributes;
  15import android.media.RingtoneManager;
  16import android.net.Uri;
  17import android.os.Build;
  18import android.os.SystemClock;
  19import android.preference.PreferenceManager;
  20import android.support.annotation.RequiresApi;
  21import android.support.v4.app.NotificationCompat;
  22import android.support.v4.app.NotificationCompat.BigPictureStyle;
  23import android.support.v4.app.NotificationCompat.Builder;
  24import android.support.v4.app.NotificationManagerCompat;
  25import android.support.v4.app.Person;
  26import android.support.v4.app.RemoteInput;
  27import android.support.v4.content.ContextCompat;
  28import android.support.v4.graphics.drawable.IconCompat;
  29import android.text.SpannableString;
  30import android.text.style.StyleSpan;
  31import android.util.DisplayMetrics;
  32import android.util.Log;
  33
  34import java.io.File;
  35import java.io.IOException;
  36import java.util.ArrayList;
  37import java.util.Calendar;
  38import java.util.Collections;
  39import java.util.HashMap;
  40import java.util.Iterator;
  41import java.util.LinkedHashMap;
  42import java.util.List;
  43import java.util.Map;
  44import java.util.Set;
  45import java.util.concurrent.atomic.AtomicInteger;
  46import java.util.regex.Matcher;
  47import java.util.regex.Pattern;
  48
  49import eu.siacs.conversations.Config;
  50import eu.siacs.conversations.R;
  51import eu.siacs.conversations.entities.Account;
  52import eu.siacs.conversations.entities.Contact;
  53import eu.siacs.conversations.entities.Conversation;
  54import eu.siacs.conversations.entities.Conversational;
  55import eu.siacs.conversations.entities.Message;
  56import eu.siacs.conversations.persistance.FileBackend;
  57import eu.siacs.conversations.ui.ConversationsActivity;
  58import eu.siacs.conversations.ui.EditAccountActivity;
  59import eu.siacs.conversations.ui.RtpSessionActivity;
  60import eu.siacs.conversations.ui.TimePreference;
  61import eu.siacs.conversations.utils.AccountUtils;
  62import eu.siacs.conversations.utils.Compatibility;
  63import eu.siacs.conversations.utils.GeoHelper;
  64import eu.siacs.conversations.utils.TorServiceUtils;
  65import eu.siacs.conversations.utils.UIHelper;
  66import eu.siacs.conversations.xmpp.XmppConnection;
  67import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection;
  68import eu.siacs.conversations.xmpp.jingle.Media;
  69
  70public class NotificationService {
  71
  72    public static final Object CATCHUP_LOCK = new Object();
  73
  74    private static final int LED_COLOR = 0xff00ff00;
  75
  76    private static final int CALL_DAT = 120;
  77    private static final long[] CALL_PATTERN = {0, 3 * CALL_DAT, CALL_DAT, CALL_DAT, 3 * CALL_DAT, CALL_DAT, CALL_DAT};
  78
  79    private static final String CONVERSATIONS_GROUP = "eu.siacs.conversations";
  80    private static final int NOTIFICATION_ID_MULTIPLIER = 1024 * 1024;
  81    static final int FOREGROUND_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 4;
  82    private static final int NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 2;
  83    private static final int ERROR_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 6;
  84    private static final int INCOMING_CALL_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 8;
  85    public static final int ONGOING_CALL_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 10;
  86    private final XmppConnectionService mXmppConnectionService;
  87    private final LinkedHashMap<String, ArrayList<Message>> notifications = new LinkedHashMap<>();
  88    private final HashMap<Conversation, AtomicInteger> mBacklogMessageCounter = new HashMap<>();
  89    private Conversation mOpenConversation;
  90    private boolean mIsInForeground;
  91    private long mLastNotification;
  92
  93    NotificationService(final XmppConnectionService service) {
  94        this.mXmppConnectionService = service;
  95    }
  96
  97    private static boolean displaySnoozeAction(List<Message> messages) {
  98        int numberOfMessagesWithoutReply = 0;
  99        for (Message message : messages) {
 100            if (message.getStatus() == Message.STATUS_RECEIVED) {
 101                ++numberOfMessagesWithoutReply;
 102            } else {
 103                return false;
 104            }
 105        }
 106        return numberOfMessagesWithoutReply >= 3;
 107    }
 108
 109    public static Pattern generateNickHighlightPattern(final String nick) {
 110        return Pattern.compile("(?<=(^|\\s))" + Pattern.quote(nick) + "(?=\\s|$|\\p{Punct})");
 111    }
 112
 113    private static boolean isImageMessage(Message message) {
 114        return message.getType() != Message.TYPE_TEXT
 115                && message.getTransferable() == null
 116                && !message.isDeleted()
 117                && message.getEncryption() != Message.ENCRYPTION_PGP
 118                && message.getFileParams().height > 0;
 119    }
 120
 121    @RequiresApi(api = Build.VERSION_CODES.O)
 122    void initializeChannels() {
 123        final Context c = mXmppConnectionService;
 124        final NotificationManager notificationManager = c.getSystemService(NotificationManager.class);
 125        if (notificationManager == null) {
 126            return;
 127        }
 128
 129        notificationManager.deleteNotificationChannel("export");
 130
 131        notificationManager.createNotificationChannelGroup(new NotificationChannelGroup("status", c.getString(R.string.notification_group_status_information)));
 132        notificationManager.createNotificationChannelGroup(new NotificationChannelGroup("chats", c.getString(R.string.notification_group_messages)));
 133        notificationManager.createNotificationChannelGroup(new NotificationChannelGroup("calls", c.getString(R.string.notification_group_calls)));
 134        final NotificationChannel foregroundServiceChannel = new NotificationChannel("foreground",
 135                c.getString(R.string.foreground_service_channel_name),
 136                NotificationManager.IMPORTANCE_MIN);
 137        foregroundServiceChannel.setDescription(c.getString(R.string.foreground_service_channel_description));
 138        foregroundServiceChannel.setShowBadge(false);
 139        foregroundServiceChannel.setGroup("status");
 140        notificationManager.createNotificationChannel(foregroundServiceChannel);
 141        final NotificationChannel errorChannel = new NotificationChannel("error",
 142                c.getString(R.string.error_channel_name),
 143                NotificationManager.IMPORTANCE_LOW);
 144        errorChannel.setDescription(c.getString(R.string.error_channel_description));
 145        errorChannel.setShowBadge(false);
 146        errorChannel.setGroup("status");
 147        notificationManager.createNotificationChannel(errorChannel);
 148
 149        final NotificationChannel videoCompressionChannel = new NotificationChannel("compression",
 150                c.getString(R.string.video_compression_channel_name),
 151                NotificationManager.IMPORTANCE_LOW);
 152        videoCompressionChannel.setShowBadge(false);
 153        videoCompressionChannel.setGroup("status");
 154        notificationManager.createNotificationChannel(videoCompressionChannel);
 155
 156        final NotificationChannel exportChannel = new NotificationChannel("backup",
 157                c.getString(R.string.backup_channel_name),
 158                NotificationManager.IMPORTANCE_LOW);
 159        exportChannel.setShowBadge(false);
 160        exportChannel.setGroup("status");
 161        notificationManager.createNotificationChannel(exportChannel);
 162
 163        final NotificationChannel incomingCallsChannel = new NotificationChannel("incoming_calls",
 164                c.getString(R.string.incoming_calls_channel_name),
 165                NotificationManager.IMPORTANCE_HIGH);
 166        incomingCallsChannel.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE), new AudioAttributes.Builder()
 167                .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
 168                .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
 169                .build());
 170        incomingCallsChannel.setShowBadge(false);
 171        incomingCallsChannel.setLightColor(LED_COLOR);
 172        incomingCallsChannel.enableLights(true);
 173        incomingCallsChannel.setGroup("calls");
 174        incomingCallsChannel.setBypassDnd(true);
 175        incomingCallsChannel.enableVibration(true);
 176        incomingCallsChannel.setVibrationPattern(CALL_PATTERN);
 177        notificationManager.createNotificationChannel(incomingCallsChannel);
 178
 179        final NotificationChannel ongoingCallsChannel = new NotificationChannel("ongoing_calls",
 180                c.getString(R.string.ongoing_calls_channel_name),
 181                NotificationManager.IMPORTANCE_LOW);
 182        ongoingCallsChannel.setShowBadge(false);
 183        ongoingCallsChannel.setGroup("calls");
 184        notificationManager.createNotificationChannel(ongoingCallsChannel);
 185
 186
 187        final NotificationChannel messagesChannel = new NotificationChannel("messages",
 188                c.getString(R.string.messages_channel_name),
 189                NotificationManager.IMPORTANCE_HIGH);
 190        messagesChannel.setShowBadge(true);
 191        messagesChannel.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION), new AudioAttributes.Builder()
 192                .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
 193                .setUsage(AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT)
 194                .build());
 195        messagesChannel.setLightColor(LED_COLOR);
 196        final int dat = 70;
 197        final long[] pattern = {0, 3 * dat, dat, dat};
 198        messagesChannel.setVibrationPattern(pattern);
 199        messagesChannel.enableVibration(true);
 200        messagesChannel.enableLights(true);
 201        messagesChannel.setGroup("chats");
 202        notificationManager.createNotificationChannel(messagesChannel);
 203        final NotificationChannel silentMessagesChannel = new NotificationChannel("silent_messages",
 204                c.getString(R.string.silent_messages_channel_name),
 205                NotificationManager.IMPORTANCE_LOW);
 206        silentMessagesChannel.setDescription(c.getString(R.string.silent_messages_channel_description));
 207        silentMessagesChannel.setShowBadge(true);
 208        silentMessagesChannel.setLightColor(LED_COLOR);
 209        silentMessagesChannel.enableLights(true);
 210        silentMessagesChannel.setGroup("chats");
 211        notificationManager.createNotificationChannel(silentMessagesChannel);
 212
 213        final NotificationChannel quietHoursChannel = new NotificationChannel("quiet_hours",
 214                c.getString(R.string.title_pref_quiet_hours),
 215                NotificationManager.IMPORTANCE_LOW);
 216        quietHoursChannel.setShowBadge(true);
 217        quietHoursChannel.setLightColor(LED_COLOR);
 218        quietHoursChannel.enableLights(true);
 219        quietHoursChannel.setGroup("chats");
 220        quietHoursChannel.enableVibration(false);
 221        quietHoursChannel.setSound(null, null);
 222
 223        notificationManager.createNotificationChannel(quietHoursChannel);
 224    }
 225
 226    public boolean notify(final Message message) {
 227        final Conversation conversation = (Conversation) message.getConversation();
 228        return message.getStatus() == Message.STATUS_RECEIVED
 229                && !conversation.isMuted()
 230                && (conversation.alwaysNotify() || wasHighlightedOrPrivate(message))
 231                && (!conversation.isWithStranger() || notificationsFromStrangers());
 232    }
 233
 234    public boolean notificationsFromStrangers() {
 235        return mXmppConnectionService.getBooleanPreference("notifications_from_strangers", R.bool.notifications_from_strangers);
 236    }
 237
 238    private boolean isQuietHours() {
 239        if (!mXmppConnectionService.getBooleanPreference("enable_quiet_hours", R.bool.enable_quiet_hours)) {
 240            return false;
 241        }
 242        final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mXmppConnectionService);
 243        final long startTime = TimePreference.minutesToTimestamp(preferences.getLong("quiet_hours_start", TimePreference.DEFAULT_VALUE));
 244        final long endTime = TimePreference.minutesToTimestamp(preferences.getLong("quiet_hours_end", TimePreference.DEFAULT_VALUE));
 245        final long nowTime = Calendar.getInstance().getTimeInMillis();
 246
 247        if (endTime < startTime) {
 248            return nowTime > startTime || nowTime < endTime;
 249        } else {
 250            return nowTime > startTime && nowTime < endTime;
 251        }
 252    }
 253
 254    public void pushFromBacklog(final Message message) {
 255        if (notify(message)) {
 256            synchronized (notifications) {
 257                getBacklogMessageCounter((Conversation) message.getConversation()).incrementAndGet();
 258                pushToStack(message);
 259            }
 260        }
 261    }
 262
 263    private AtomicInteger getBacklogMessageCounter(Conversation conversation) {
 264        synchronized (mBacklogMessageCounter) {
 265            if (!mBacklogMessageCounter.containsKey(conversation)) {
 266                mBacklogMessageCounter.put(conversation, new AtomicInteger(0));
 267            }
 268            return mBacklogMessageCounter.get(conversation);
 269        }
 270    }
 271
 272    void pushFromDirectReply(final Message message) {
 273        synchronized (notifications) {
 274            pushToStack(message);
 275            updateNotification(false);
 276        }
 277    }
 278
 279    public void finishBacklog(boolean notify, Account account) {
 280        synchronized (notifications) {
 281            mXmppConnectionService.updateUnreadCountBadge();
 282            if (account == null || !notify) {
 283                updateNotification(notify);
 284            } else {
 285                final int count;
 286                final List<String> conversations;
 287                synchronized (this.mBacklogMessageCounter) {
 288                    conversations = getBacklogConversations(account);
 289                    count = getBacklogMessageCount(account);
 290                }
 291                updateNotification(count > 0, conversations);
 292            }
 293        }
 294    }
 295
 296    private List<String> getBacklogConversations(Account account) {
 297        final List<String> conversations = new ArrayList<>();
 298        for (Map.Entry<Conversation, AtomicInteger> entry : mBacklogMessageCounter.entrySet()) {
 299            if (entry.getKey().getAccount() == account) {
 300                conversations.add(entry.getKey().getUuid());
 301            }
 302        }
 303        return conversations;
 304    }
 305
 306    private int getBacklogMessageCount(Account account) {
 307        int count = 0;
 308        for (Iterator<Map.Entry<Conversation, AtomicInteger>> it = mBacklogMessageCounter.entrySet().iterator(); it.hasNext(); ) {
 309            Map.Entry<Conversation, AtomicInteger> entry = it.next();
 310            if (entry.getKey().getAccount() == account) {
 311                count += entry.getValue().get();
 312                it.remove();
 313            }
 314        }
 315        Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": backlog message count=" + count);
 316        return count;
 317    }
 318
 319    void finishBacklog(boolean notify) {
 320        finishBacklog(notify, null);
 321    }
 322
 323    private void pushToStack(final Message message) {
 324        final String conversationUuid = message.getConversationUuid();
 325        if (notifications.containsKey(conversationUuid)) {
 326            notifications.get(conversationUuid).add(message);
 327        } else {
 328            final ArrayList<Message> mList = new ArrayList<>();
 329            mList.add(message);
 330            notifications.put(conversationUuid, mList);
 331        }
 332    }
 333
 334    public void push(final Message message) {
 335        synchronized (CATCHUP_LOCK) {
 336            final XmppConnection connection = message.getConversation().getAccount().getXmppConnection();
 337            if (connection != null && connection.isWaitingForSmCatchup()) {
 338                connection.incrementSmCatchupMessageCounter();
 339                pushFromBacklog(message);
 340            } else {
 341                pushNow(message);
 342            }
 343        }
 344    }
 345
 346    public void showIncomingCallNotification(final AbstractJingleConnection.Id id, final Set<Media> media) {
 347        final Intent fullScreenIntent = new Intent(mXmppConnectionService, RtpSessionActivity.class);
 348        fullScreenIntent.putExtra(RtpSessionActivity.EXTRA_ACCOUNT, id.account.getJid().asBareJid().toEscapedString());
 349        fullScreenIntent.putExtra(RtpSessionActivity.EXTRA_WITH, id.with.toEscapedString());
 350        fullScreenIntent.putExtra(RtpSessionActivity.EXTRA_SESSION_ID, id.sessionId);
 351        fullScreenIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
 352        fullScreenIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
 353        final NotificationCompat.Builder builder = new NotificationCompat.Builder(mXmppConnectionService, "incoming_calls");
 354        if (media.contains(Media.VIDEO)) {
 355            builder.setSmallIcon(R.drawable.ic_videocam_white_24dp);
 356            builder.setContentTitle(mXmppConnectionService.getString(R.string.rtp_state_incoming_video_call));
 357        } else {
 358            builder.setSmallIcon(R.drawable.ic_call_white_24dp);
 359            builder.setContentTitle(mXmppConnectionService.getString(R.string.rtp_state_incoming_call));
 360        }
 361        final Contact contact = id.getContact();
 362        builder.setLargeIcon(mXmppConnectionService.getAvatarService().get(
 363                contact,
 364                AvatarService.getSystemUiAvatarSize(mXmppConnectionService))
 365        );
 366        final Uri systemAccount = contact.getSystemAccount();
 367        if (systemAccount != null) {
 368            builder.addPerson(systemAccount.toString());
 369        }
 370        builder.setContentText(id.account.getRoster().getContact(id.with).getDisplayName());
 371        builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC);
 372        builder.setPriority(NotificationCompat.PRIORITY_HIGH);
 373        builder.setCategory(NotificationCompat.CATEGORY_CALL);
 374        PendingIntent pendingIntent = createPendingRtpSession(id, Intent.ACTION_VIEW, 101);
 375        builder.setFullScreenIntent(pendingIntent, true);
 376        builder.setContentIntent(pendingIntent); //old androids need this?
 377        builder.setOngoing(true);
 378        builder.addAction(new NotificationCompat.Action.Builder(
 379                R.drawable.ic_call_end_white_48dp,
 380                mXmppConnectionService.getString(R.string.dismiss_call),
 381                createCallAction(id.sessionId, XmppConnectionService.ACTION_DISMISS_CALL, 102))
 382                .build());
 383        builder.addAction(new NotificationCompat.Action.Builder(
 384                R.drawable.ic_call_white_24dp,
 385                mXmppConnectionService.getString(R.string.answer_call),
 386                createPendingRtpSession(id, RtpSessionActivity.ACTION_ACCEPT_CALL, 103))
 387                .build());
 388        modifyIncomingCall(builder);
 389        final Notification notification = builder.build();
 390        notification.flags = notification.flags | Notification.FLAG_INSISTENT;
 391        notify(INCOMING_CALL_NOTIFICATION_ID, notification);
 392    }
 393
 394    public Notification getOngoingCallNotification(final AbstractJingleConnection.Id id, final Set<Media> media) {
 395        final NotificationCompat.Builder builder = new NotificationCompat.Builder(mXmppConnectionService, "ongoing_calls");
 396        if (media.contains(Media.VIDEO)) {
 397            builder.setSmallIcon(R.drawable.ic_videocam_white_24dp);
 398            builder.setContentTitle(mXmppConnectionService.getString(R.string.ongoing_video_call));
 399        } else {
 400            builder.setSmallIcon(R.drawable.ic_call_white_24dp);
 401            builder.setContentTitle(mXmppConnectionService.getString(R.string.ongoing_call));
 402        }
 403        builder.setContentText(id.account.getRoster().getContact(id.with).getDisplayName());
 404        builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC);
 405        builder.setPriority(NotificationCompat.PRIORITY_HIGH);
 406        builder.setCategory(NotificationCompat.CATEGORY_CALL);
 407        builder.setContentIntent(createPendingRtpSession(id, Intent.ACTION_VIEW, 101));
 408        builder.setOngoing(true);
 409        builder.addAction(new NotificationCompat.Action.Builder(
 410                R.drawable.ic_call_end_white_48dp,
 411                mXmppConnectionService.getString(R.string.hang_up),
 412                createCallAction(id.sessionId, XmppConnectionService.ACTION_END_CALL, 104))
 413                .build());
 414        return builder.build();
 415    }
 416
 417    private PendingIntent createPendingRtpSession(final AbstractJingleConnection.Id id, final String action, final int requestCode) {
 418        final Intent fullScreenIntent = new Intent(mXmppConnectionService, RtpSessionActivity.class);
 419        fullScreenIntent.setAction(action);
 420        fullScreenIntent.putExtra(RtpSessionActivity.EXTRA_ACCOUNT, id.account.getJid().asBareJid().toEscapedString());
 421        fullScreenIntent.putExtra(RtpSessionActivity.EXTRA_WITH, id.with.toEscapedString());
 422        fullScreenIntent.putExtra(RtpSessionActivity.EXTRA_SESSION_ID, id.sessionId);
 423        return PendingIntent.getActivity(mXmppConnectionService, requestCode, fullScreenIntent, PendingIntent.FLAG_UPDATE_CURRENT);
 424    }
 425
 426    public void cancelIncomingCallNotification() {
 427        cancel(INCOMING_CALL_NOTIFICATION_ID);
 428    }
 429
 430    public static void cancelIncomingCallNotification(final Context context) {
 431        final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
 432        try {
 433            notificationManager.cancel(INCOMING_CALL_NOTIFICATION_ID);
 434        } catch (RuntimeException e) {
 435            Log.d(Config.LOGTAG, "unable to cancel incoming call notification after crash", e);
 436        }
 437    }
 438
 439    private void pushNow(final Message message) {
 440        mXmppConnectionService.updateUnreadCountBadge();
 441        if (!notify(message)) {
 442            Log.d(Config.LOGTAG, message.getConversation().getAccount().getJid().asBareJid() + ": suppressing notification because turned off");
 443            return;
 444        }
 445        final boolean isScreenOn = mXmppConnectionService.isInteractive();
 446        if (this.mIsInForeground && isScreenOn && this.mOpenConversation == message.getConversation()) {
 447            Log.d(Config.LOGTAG, message.getConversation().getAccount().getJid().asBareJid() + ": suppressing notification because conversation is open");
 448            return;
 449        }
 450        synchronized (notifications) {
 451            pushToStack(message);
 452            final Conversational conversation = message.getConversation();
 453            final Account account = conversation.getAccount();
 454            final boolean doNotify = (!(this.mIsInForeground && this.mOpenConversation == null) || !isScreenOn)
 455                    && !account.inGracePeriod()
 456                    && !this.inMiniGracePeriod(account);
 457            updateNotification(doNotify, Collections.singletonList(conversation.getUuid()));
 458        }
 459    }
 460
 461    public void clear() {
 462        synchronized (notifications) {
 463            for (ArrayList<Message> messages : notifications.values()) {
 464                markAsReadIfHasDirectReply(messages);
 465            }
 466            notifications.clear();
 467            updateNotification(false);
 468        }
 469    }
 470
 471    public void clear(final Conversation conversation) {
 472        synchronized (this.mBacklogMessageCounter) {
 473            this.mBacklogMessageCounter.remove(conversation);
 474        }
 475        synchronized (notifications) {
 476            markAsReadIfHasDirectReply(conversation);
 477            if (notifications.remove(conversation.getUuid()) != null) {
 478                cancel(conversation.getUuid(), NOTIFICATION_ID);
 479                updateNotification(false, null, true);
 480            }
 481        }
 482    }
 483
 484    private void markAsReadIfHasDirectReply(final Conversation conversation) {
 485        markAsReadIfHasDirectReply(notifications.get(conversation.getUuid()));
 486    }
 487
 488    private void markAsReadIfHasDirectReply(final ArrayList<Message> messages) {
 489        if (messages != null && messages.size() > 0) {
 490            Message last = messages.get(messages.size() - 1);
 491            if (last.getStatus() != Message.STATUS_RECEIVED) {
 492                if (mXmppConnectionService.markRead((Conversation) last.getConversation(), false)) {
 493                    mXmppConnectionService.updateConversationUi();
 494                }
 495            }
 496        }
 497    }
 498
 499    private void setNotificationColor(final Builder mBuilder) {
 500        mBuilder.setColor(ContextCompat.getColor(mXmppConnectionService, R.color.green600));
 501    }
 502
 503    public void updateNotification() {
 504        synchronized (notifications) {
 505            updateNotification(false);
 506        }
 507    }
 508
 509    private void updateNotification(final boolean notify) {
 510        updateNotification(notify, null, false);
 511    }
 512
 513    private void updateNotification(final boolean notify, final List<String> conversations) {
 514        updateNotification(notify, conversations, false);
 515    }
 516
 517    private void updateNotification(final boolean notify, final List<String> conversations, final boolean summaryOnly) {
 518        final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mXmppConnectionService);
 519
 520        final boolean quiteHours = isQuietHours();
 521
 522        final boolean notifyOnlyOneChild = notify && conversations != null && conversations.size() == 1; //if this check is changed to > 0 catchup messages will create one notification per conversation
 523
 524
 525        if (notifications.size() == 0) {
 526            cancel(NOTIFICATION_ID);
 527        } else {
 528            if (notify) {
 529                this.markLastNotification();
 530            }
 531            final Builder mBuilder;
 532            if (notifications.size() == 1 && Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
 533                mBuilder = buildSingleConversations(notifications.values().iterator().next(), notify, quiteHours);
 534                modifyForSoundVibrationAndLight(mBuilder, notify, quiteHours, preferences);
 535                notify(NOTIFICATION_ID, mBuilder.build());
 536            } else {
 537                mBuilder = buildMultipleConversation(notify, quiteHours);
 538                if (notifyOnlyOneChild) {
 539                    mBuilder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN);
 540                }
 541                modifyForSoundVibrationAndLight(mBuilder, notify, quiteHours, preferences);
 542                if (!summaryOnly) {
 543                    for (Map.Entry<String, ArrayList<Message>> entry : notifications.entrySet()) {
 544                        String uuid = entry.getKey();
 545                        final boolean notifyThis = notifyOnlyOneChild ? conversations.contains(uuid) : notify;
 546                        Builder singleBuilder = buildSingleConversations(entry.getValue(), notifyThis, quiteHours);
 547                        if (!notifyOnlyOneChild) {
 548                            singleBuilder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY);
 549                        }
 550                        modifyForSoundVibrationAndLight(singleBuilder, notifyThis, quiteHours, preferences);
 551                        singleBuilder.setGroup(CONVERSATIONS_GROUP);
 552                        setNotificationColor(singleBuilder);
 553                        notify(entry.getKey(), NOTIFICATION_ID, singleBuilder.build());
 554                    }
 555                }
 556                notify(NOTIFICATION_ID, mBuilder.build());
 557            }
 558        }
 559    }
 560
 561    private void modifyForSoundVibrationAndLight(Builder mBuilder, boolean notify, boolean quietHours, SharedPreferences preferences) {
 562        final Resources resources = mXmppConnectionService.getResources();
 563        final String ringtone = preferences.getString("notification_ringtone", resources.getString(R.string.notification_ringtone));
 564        final boolean vibrate = preferences.getBoolean("vibrate_on_notification", resources.getBoolean(R.bool.vibrate_on_notification));
 565        final boolean led = preferences.getBoolean("led", resources.getBoolean(R.bool.led));
 566        final boolean headsup = preferences.getBoolean("notification_headsup", resources.getBoolean(R.bool.headsup_notifications));
 567        if (notify && !quietHours) {
 568            if (vibrate) {
 569                final int dat = 70;
 570                final long[] pattern = {0, 3 * dat, dat, dat};
 571                mBuilder.setVibrate(pattern);
 572            } else {
 573                mBuilder.setVibrate(new long[]{0});
 574            }
 575            Uri uri = Uri.parse(ringtone);
 576            try {
 577                mBuilder.setSound(fixRingtoneUri(uri));
 578            } catch (SecurityException e) {
 579                Log.d(Config.LOGTAG, "unable to use custom notification sound " + uri.toString());
 580            }
 581        } else {
 582            mBuilder.setLocalOnly(true);
 583        }
 584        if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
 585            mBuilder.setCategory(Notification.CATEGORY_MESSAGE);
 586        }
 587        mBuilder.setPriority(notify ? (headsup ? NotificationCompat.PRIORITY_HIGH : NotificationCompat.PRIORITY_DEFAULT) : NotificationCompat.PRIORITY_LOW);
 588        setNotificationColor(mBuilder);
 589        mBuilder.setDefaults(0);
 590        if (led) {
 591            mBuilder.setLights(LED_COLOR, 2000, 3000);
 592        }
 593    }
 594
 595    private void modifyIncomingCall(Builder mBuilder) {
 596        final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mXmppConnectionService);
 597        final Resources resources = mXmppConnectionService.getResources();
 598        final String ringtone = preferences.getString("call_ringtone", resources.getString(R.string.incoming_call_ringtone));
 599        mBuilder.setVibrate(CALL_PATTERN);
 600        final Uri uri = Uri.parse(ringtone);
 601        try {
 602            mBuilder.setSound(fixRingtoneUri(uri));
 603        } catch (SecurityException e) {
 604            Log.d(Config.LOGTAG, "unable to use custom notification sound " + uri.toString());
 605        }
 606        mBuilder.setPriority(NotificationCompat.PRIORITY_HIGH);
 607        setNotificationColor(mBuilder);
 608        mBuilder.setLights(LED_COLOR, 2000, 3000);
 609    }
 610
 611    private Uri fixRingtoneUri(Uri uri) {
 612        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && "file".equals(uri.getScheme())) {
 613            return FileBackend.getUriForFile(mXmppConnectionService, new File(uri.getPath()));
 614        } else {
 615            return uri;
 616        }
 617    }
 618
 619    private Builder buildMultipleConversation(final boolean notify, final boolean quietHours) {
 620        final Builder mBuilder = new NotificationCompat.Builder(mXmppConnectionService, quietHours ? "quiet_hours" : (notify ? "messages" : "silent_messages"));
 621        final NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle();
 622        style.setBigContentTitle(mXmppConnectionService.getString(R.string.x_unread_conversations, notifications.size()));
 623        final StringBuilder names = new StringBuilder();
 624        Conversation conversation = null;
 625        for (final ArrayList<Message> messages : notifications.values()) {
 626            if (messages.size() > 0) {
 627                conversation = (Conversation) messages.get(0).getConversation();
 628                final String name = conversation.getName().toString();
 629                SpannableString styledString;
 630                if (Config.HIDE_MESSAGE_TEXT_IN_NOTIFICATION) {
 631                    int count = messages.size();
 632                    styledString = new SpannableString(name + ": " + mXmppConnectionService.getResources().getQuantityString(R.plurals.x_messages, count, count));
 633                    styledString.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), 0);
 634                    style.addLine(styledString);
 635                } else {
 636                    styledString = new SpannableString(name + ": " + UIHelper.getMessagePreview(mXmppConnectionService, messages.get(0)).first);
 637                    styledString.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), 0);
 638                    style.addLine(styledString);
 639                }
 640                names.append(name);
 641                names.append(", ");
 642            }
 643        }
 644        if (names.length() >= 2) {
 645            names.delete(names.length() - 2, names.length());
 646        }
 647        mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.x_unread_conversations, notifications.size()));
 648        mBuilder.setTicker(mXmppConnectionService.getString(R.string.x_unread_conversations, notifications.size()));
 649        mBuilder.setContentText(names.toString());
 650        mBuilder.setStyle(style);
 651        if (conversation != null) {
 652            mBuilder.setContentIntent(createContentIntent(conversation));
 653        }
 654        mBuilder.setGroupSummary(true);
 655        mBuilder.setGroup(CONVERSATIONS_GROUP);
 656        mBuilder.setDeleteIntent(createDeleteIntent(null));
 657        mBuilder.setSmallIcon(R.drawable.ic_notification);
 658        return mBuilder;
 659    }
 660
 661    private Builder buildSingleConversations(final ArrayList<Message> messages, final boolean notify, final boolean quietHours) {
 662        final Builder mBuilder = new NotificationCompat.Builder(mXmppConnectionService, quietHours ? "quiet_hours" : (notify ? "messages" : "silent_messages"));
 663        if (messages.size() >= 1) {
 664            final Conversation conversation = (Conversation) messages.get(0).getConversation();
 665            mBuilder.setLargeIcon(mXmppConnectionService.getAvatarService()
 666                    .get(conversation, AvatarService.getSystemUiAvatarSize(mXmppConnectionService)));
 667            mBuilder.setContentTitle(conversation.getName());
 668            if (Config.HIDE_MESSAGE_TEXT_IN_NOTIFICATION) {
 669                int count = messages.size();
 670                mBuilder.setContentText(mXmppConnectionService.getResources().getQuantityString(R.plurals.x_messages, count, count));
 671            } else {
 672                Message message;
 673                //TODO starting with Android 9 we might want to put images in MessageStyle
 674                if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P && (message = getImage(messages)) != null) {
 675                    modifyForImage(mBuilder, message, messages);
 676                } else {
 677                    modifyForTextOnly(mBuilder, messages);
 678                }
 679                RemoteInput remoteInput = new RemoteInput.Builder("text_reply").setLabel(UIHelper.getMessageHint(mXmppConnectionService, conversation)).build();
 680                PendingIntent markAsReadPendingIntent = createReadPendingIntent(conversation);
 681                NotificationCompat.Action markReadAction = new NotificationCompat.Action.Builder(
 682                        R.drawable.ic_drafts_white_24dp,
 683                        mXmppConnectionService.getString(R.string.mark_as_read),
 684                        markAsReadPendingIntent)
 685                        .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_MARK_AS_READ)
 686                        .setShowsUserInterface(false)
 687                        .build();
 688                String replyLabel = mXmppConnectionService.getString(R.string.reply);
 689                NotificationCompat.Action replyAction = new NotificationCompat.Action.Builder(
 690                        R.drawable.ic_send_text_offline,
 691                        replyLabel,
 692                        createReplyIntent(conversation, false))
 693                        .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY)
 694                        .setShowsUserInterface(false)
 695                        .addRemoteInput(remoteInput).build();
 696                NotificationCompat.Action wearReplyAction = new NotificationCompat.Action.Builder(R.drawable.ic_wear_reply,
 697                        replyLabel,
 698                        createReplyIntent(conversation, true)).addRemoteInput(remoteInput).build();
 699                mBuilder.extend(new NotificationCompat.WearableExtender().addAction(wearReplyAction));
 700                int addedActionsCount = 1;
 701                mBuilder.addAction(markReadAction);
 702                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
 703                    mBuilder.addAction(replyAction);
 704                    ++addedActionsCount;
 705                }
 706
 707                if (displaySnoozeAction(messages)) {
 708                    String label = mXmppConnectionService.getString(R.string.snooze);
 709                    PendingIntent pendingSnoozeIntent = createSnoozeIntent(conversation);
 710                    NotificationCompat.Action snoozeAction = new NotificationCompat.Action.Builder(
 711                            R.drawable.ic_notifications_paused_white_24dp,
 712                            label,
 713                            pendingSnoozeIntent).build();
 714                    mBuilder.addAction(snoozeAction);
 715                    ++addedActionsCount;
 716                }
 717                if (addedActionsCount < 3) {
 718                    final Message firstLocationMessage = getFirstLocationMessage(messages);
 719                    if (firstLocationMessage != null) {
 720                        final PendingIntent pendingShowLocationIntent = createShowLocationIntent(firstLocationMessage);
 721                        if (pendingShowLocationIntent != null) {
 722                            final String label = mXmppConnectionService.getResources().getString(R.string.show_location);
 723                            NotificationCompat.Action locationAction = new NotificationCompat.Action.Builder(
 724                                    R.drawable.ic_room_white_24dp,
 725                                    label,
 726                                    pendingShowLocationIntent).build();
 727                            mBuilder.addAction(locationAction);
 728                            ++addedActionsCount;
 729                        }
 730                    }
 731                }
 732                if (addedActionsCount < 3) {
 733                    Message firstDownloadableMessage = getFirstDownloadableMessage(messages);
 734                    if (firstDownloadableMessage != null) {
 735                        String label = mXmppConnectionService.getResources().getString(R.string.download_x_file, UIHelper.getFileDescriptionString(mXmppConnectionService, firstDownloadableMessage));
 736                        PendingIntent pendingDownloadIntent = createDownloadIntent(firstDownloadableMessage);
 737                        NotificationCompat.Action downloadAction = new NotificationCompat.Action.Builder(
 738                                R.drawable.ic_file_download_white_24dp,
 739                                label,
 740                                pendingDownloadIntent).build();
 741                        mBuilder.addAction(downloadAction);
 742                        ++addedActionsCount;
 743                    }
 744                }
 745            }
 746            if (conversation.getMode() == Conversation.MODE_SINGLE) {
 747                Contact contact = conversation.getContact();
 748                Uri systemAccount = contact.getSystemAccount();
 749                if (systemAccount != null) {
 750                    mBuilder.addPerson(systemAccount.toString());
 751                }
 752            }
 753            mBuilder.setWhen(conversation.getLatestMessage().getTimeSent());
 754            mBuilder.setSmallIcon(R.drawable.ic_notification);
 755            mBuilder.setDeleteIntent(createDeleteIntent(conversation));
 756            mBuilder.setContentIntent(createContentIntent(conversation));
 757        }
 758        return mBuilder;
 759    }
 760
 761    private void modifyForImage(final Builder builder, final Message message, final ArrayList<Message> messages) {
 762        try {
 763            final Bitmap bitmap = mXmppConnectionService.getFileBackend().getThumbnail(message, getPixel(288), false);
 764            final ArrayList<Message> tmp = new ArrayList<>();
 765            for (final Message msg : messages) {
 766                if (msg.getType() == Message.TYPE_TEXT
 767                        && msg.getTransferable() == null) {
 768                    tmp.add(msg);
 769                }
 770            }
 771            final BigPictureStyle bigPictureStyle = new NotificationCompat.BigPictureStyle();
 772            bigPictureStyle.bigPicture(bitmap);
 773            if (tmp.size() > 0) {
 774                CharSequence text = getMergedBodies(tmp);
 775                bigPictureStyle.setSummaryText(text);
 776                builder.setContentText(text);
 777                builder.setTicker(text);
 778            } else {
 779                final String description = UIHelper.getFileDescriptionString(mXmppConnectionService, message);
 780                builder.setContentText(description);
 781                builder.setTicker(description);
 782            }
 783            builder.setStyle(bigPictureStyle);
 784        } catch (final IOException e) {
 785            modifyForTextOnly(builder, messages);
 786        }
 787    }
 788
 789    private Person getPerson(Message message) {
 790        final Contact contact = message.getContact();
 791        final Person.Builder builder = new Person.Builder();
 792        if (contact != null) {
 793            builder.setName(contact.getDisplayName());
 794            final Uri uri = contact.getSystemAccount();
 795            if (uri != null) {
 796                builder.setUri(uri.toString());
 797            }
 798        } else {
 799            builder.setName(UIHelper.getMessageDisplayName(message));
 800        }
 801        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
 802            builder.setIcon(IconCompat.createWithBitmap(mXmppConnectionService.getAvatarService().get(message, AvatarService.getSystemUiAvatarSize(mXmppConnectionService), false)));
 803        }
 804        return builder.build();
 805    }
 806
 807    private void modifyForTextOnly(final Builder builder, final ArrayList<Message> messages) {
 808        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
 809            final Conversation conversation = (Conversation) messages.get(0).getConversation();
 810            final Person.Builder meBuilder = new Person.Builder().setName(mXmppConnectionService.getString(R.string.me));
 811            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
 812                meBuilder.setIcon(IconCompat.createWithBitmap(mXmppConnectionService.getAvatarService().get(conversation.getAccount(), AvatarService.getSystemUiAvatarSize(mXmppConnectionService))));
 813            }
 814            final Person me = meBuilder.build();
 815            NotificationCompat.MessagingStyle messagingStyle = new NotificationCompat.MessagingStyle(me);
 816            final boolean multiple = conversation.getMode() == Conversation.MODE_MULTI;
 817            if (multiple) {
 818                messagingStyle.setConversationTitle(conversation.getName());
 819            }
 820            for (Message message : messages) {
 821                final Person sender = message.getStatus() == Message.STATUS_RECEIVED ? getPerson(message) : null;
 822                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && isImageMessage(message)) {
 823                    final Uri dataUri = FileBackend.getMediaUri(mXmppConnectionService, mXmppConnectionService.getFileBackend().getFile(message));
 824                    NotificationCompat.MessagingStyle.Message imageMessage = new NotificationCompat.MessagingStyle.Message(UIHelper.getMessagePreview(mXmppConnectionService, message).first, message.getTimeSent(), sender);
 825                    if (dataUri != null) {
 826                        imageMessage.setData(message.getMimeType(), dataUri);
 827                    }
 828                    messagingStyle.addMessage(imageMessage);
 829                } else {
 830                    messagingStyle.addMessage(UIHelper.getMessagePreview(mXmppConnectionService, message).first, message.getTimeSent(), sender);
 831                }
 832            }
 833            messagingStyle.setGroupConversation(multiple);
 834            builder.setStyle(messagingStyle);
 835        } else {
 836            if (messages.get(0).getConversation().getMode() == Conversation.MODE_SINGLE) {
 837                builder.setStyle(new NotificationCompat.BigTextStyle().bigText(getMergedBodies(messages)));
 838                final CharSequence preview = UIHelper.getMessagePreview(mXmppConnectionService, messages.get(messages.size() - 1)).first;
 839                builder.setContentText(preview);
 840                builder.setTicker(preview);
 841                builder.setNumber(messages.size());
 842            } else {
 843                final NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle();
 844                SpannableString styledString;
 845                for (Message message : messages) {
 846                    final String name = UIHelper.getMessageDisplayName(message);
 847                    styledString = new SpannableString(name + ": " + message.getBody());
 848                    styledString.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), 0);
 849                    style.addLine(styledString);
 850                }
 851                builder.setStyle(style);
 852                int count = messages.size();
 853                if (count == 1) {
 854                    final String name = UIHelper.getMessageDisplayName(messages.get(0));
 855                    styledString = new SpannableString(name + ": " + messages.get(0).getBody());
 856                    styledString.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), 0);
 857                    builder.setContentText(styledString);
 858                    builder.setTicker(styledString);
 859                } else {
 860                    final String text = mXmppConnectionService.getResources().getQuantityString(R.plurals.x_messages, count, count);
 861                    builder.setContentText(text);
 862                    builder.setTicker(text);
 863                }
 864            }
 865        }
 866    }
 867
 868    private Message getImage(final Iterable<Message> messages) {
 869        Message image = null;
 870        for (final Message message : messages) {
 871            if (message.getStatus() != Message.STATUS_RECEIVED) {
 872                return null;
 873            }
 874            if (isImageMessage(message)) {
 875                image = message;
 876            }
 877        }
 878        return image;
 879    }
 880
 881    private Message getFirstDownloadableMessage(final Iterable<Message> messages) {
 882        for (final Message message : messages) {
 883            if (message.getTransferable() != null || (message.getType() == Message.TYPE_TEXT && message.treatAsDownloadable())) {
 884                return message;
 885            }
 886        }
 887        return null;
 888    }
 889
 890    private Message getFirstLocationMessage(final Iterable<Message> messages) {
 891        for (final Message message : messages) {
 892            if (message.isGeoUri()) {
 893                return message;
 894            }
 895        }
 896        return null;
 897    }
 898
 899    private CharSequence getMergedBodies(final ArrayList<Message> messages) {
 900        final StringBuilder text = new StringBuilder();
 901        for (Message message : messages) {
 902            if (text.length() != 0) {
 903                text.append("\n");
 904            }
 905            text.append(UIHelper.getMessagePreview(mXmppConnectionService, message).first);
 906        }
 907        return text.toString();
 908    }
 909
 910    private PendingIntent createShowLocationIntent(final Message message) {
 911        Iterable<Intent> intents = GeoHelper.createGeoIntentsFromMessage(mXmppConnectionService, message);
 912        for (Intent intent : intents) {
 913            if (intent.resolveActivity(mXmppConnectionService.getPackageManager()) != null) {
 914                return PendingIntent.getActivity(mXmppConnectionService, generateRequestCode(message.getConversation(), 18), intent, PendingIntent.FLAG_UPDATE_CURRENT);
 915            }
 916        }
 917        return null;
 918    }
 919
 920    private PendingIntent createContentIntent(final String conversationUuid, final String downloadMessageUuid) {
 921        final Intent viewConversationIntent = new Intent(mXmppConnectionService, ConversationsActivity.class);
 922        viewConversationIntent.setAction(ConversationsActivity.ACTION_VIEW_CONVERSATION);
 923        viewConversationIntent.putExtra(ConversationsActivity.EXTRA_CONVERSATION, conversationUuid);
 924        if (downloadMessageUuid != null) {
 925            viewConversationIntent.putExtra(ConversationsActivity.EXTRA_DOWNLOAD_UUID, downloadMessageUuid);
 926            return PendingIntent.getActivity(mXmppConnectionService,
 927                    generateRequestCode(conversationUuid, 8),
 928                    viewConversationIntent,
 929                    PendingIntent.FLAG_UPDATE_CURRENT);
 930        } else {
 931            return PendingIntent.getActivity(mXmppConnectionService,
 932                    generateRequestCode(conversationUuid, 10),
 933                    viewConversationIntent,
 934                    PendingIntent.FLAG_UPDATE_CURRENT);
 935        }
 936    }
 937
 938    private int generateRequestCode(String uuid, int actionId) {
 939        return (actionId * NOTIFICATION_ID_MULTIPLIER) + (uuid.hashCode() % NOTIFICATION_ID_MULTIPLIER);
 940    }
 941
 942    private int generateRequestCode(Conversational conversation, int actionId) {
 943        return generateRequestCode(conversation.getUuid(), actionId);
 944    }
 945
 946    private PendingIntent createDownloadIntent(final Message message) {
 947        return createContentIntent(message.getConversationUuid(), message.getUuid());
 948    }
 949
 950    private PendingIntent createContentIntent(final Conversational conversation) {
 951        return createContentIntent(conversation.getUuid(), null);
 952    }
 953
 954    private PendingIntent createDeleteIntent(Conversation conversation) {
 955        final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
 956        intent.setAction(XmppConnectionService.ACTION_CLEAR_NOTIFICATION);
 957        if (conversation != null) {
 958            intent.putExtra("uuid", conversation.getUuid());
 959            return PendingIntent.getService(mXmppConnectionService, generateRequestCode(conversation, 20), intent, 0);
 960        }
 961        return PendingIntent.getService(mXmppConnectionService, 0, intent, 0);
 962    }
 963
 964    private PendingIntent createReplyIntent(Conversation conversation, boolean dismissAfterReply) {
 965        final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
 966        intent.setAction(XmppConnectionService.ACTION_REPLY_TO_CONVERSATION);
 967        intent.putExtra("uuid", conversation.getUuid());
 968        intent.putExtra("dismiss_notification", dismissAfterReply);
 969        final int id = generateRequestCode(conversation, dismissAfterReply ? 12 : 14);
 970        return PendingIntent.getService(mXmppConnectionService, id, intent, 0);
 971    }
 972
 973    private PendingIntent createReadPendingIntent(Conversation conversation) {
 974        final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
 975        intent.setAction(XmppConnectionService.ACTION_MARK_AS_READ);
 976        intent.putExtra("uuid", conversation.getUuid());
 977        intent.setPackage(mXmppConnectionService.getPackageName());
 978        return PendingIntent.getService(mXmppConnectionService, generateRequestCode(conversation, 16), intent, PendingIntent.FLAG_UPDATE_CURRENT);
 979    }
 980
 981    private PendingIntent createCallAction(String sessionId, final String action, int requestCode) {
 982        final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
 983        intent.setAction(action);
 984        intent.setPackage(mXmppConnectionService.getPackageName());
 985        intent.putExtra(RtpSessionActivity.EXTRA_SESSION_ID, sessionId);
 986        return PendingIntent.getService(mXmppConnectionService, requestCode, intent, PendingIntent.FLAG_UPDATE_CURRENT);
 987    }
 988
 989    private PendingIntent createSnoozeIntent(Conversation conversation) {
 990        final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
 991        intent.setAction(XmppConnectionService.ACTION_SNOOZE);
 992        intent.putExtra("uuid", conversation.getUuid());
 993        intent.setPackage(mXmppConnectionService.getPackageName());
 994        return PendingIntent.getService(mXmppConnectionService, generateRequestCode(conversation, 22), intent, PendingIntent.FLAG_UPDATE_CURRENT);
 995    }
 996
 997    private PendingIntent createTryAgainIntent() {
 998        final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
 999        intent.setAction(XmppConnectionService.ACTION_TRY_AGAIN);
1000        return PendingIntent.getService(mXmppConnectionService, 45, intent, 0);
1001    }
1002
1003    private PendingIntent createDismissErrorIntent() {
1004        final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
1005        intent.setAction(XmppConnectionService.ACTION_DISMISS_ERROR_NOTIFICATIONS);
1006        return PendingIntent.getService(mXmppConnectionService, 69, intent, 0);
1007    }
1008
1009    private boolean wasHighlightedOrPrivate(final Message message) {
1010        if (message.getConversation() instanceof Conversation) {
1011            Conversation conversation = (Conversation) message.getConversation();
1012            final String nick = conversation.getMucOptions().getActualNick();
1013            final Pattern highlight = generateNickHighlightPattern(nick);
1014            if (message.getBody() == null || nick == null) {
1015                return false;
1016            }
1017            final Matcher m = highlight.matcher(message.getBody());
1018            return (m.find() || message.isPrivateMessage());
1019        } else {
1020            return false;
1021        }
1022    }
1023
1024    public void setOpenConversation(final Conversation conversation) {
1025        this.mOpenConversation = conversation;
1026    }
1027
1028    public void setIsInForeground(final boolean foreground) {
1029        this.mIsInForeground = foreground;
1030    }
1031
1032    private int getPixel(final int dp) {
1033        final DisplayMetrics metrics = mXmppConnectionService.getResources()
1034                .getDisplayMetrics();
1035        return ((int) (dp * metrics.density));
1036    }
1037
1038    private void markLastNotification() {
1039        this.mLastNotification = SystemClock.elapsedRealtime();
1040    }
1041
1042    private boolean inMiniGracePeriod(final Account account) {
1043        final int miniGrace = account.getStatus() == Account.State.ONLINE ? Config.MINI_GRACE_PERIOD
1044                : Config.MINI_GRACE_PERIOD * 2;
1045        return SystemClock.elapsedRealtime() < (this.mLastNotification + miniGrace);
1046    }
1047
1048    Notification createForegroundNotification() {
1049        final Notification.Builder mBuilder = new Notification.Builder(mXmppConnectionService);
1050        mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.app_name));
1051        final List<Account> accounts = mXmppConnectionService.getAccounts();
1052        int enabled = 0;
1053        int connected = 0;
1054        if (accounts != null) {
1055            for (Account account : accounts) {
1056                if (account.isOnlineAndConnected()) {
1057                    connected++;
1058                    enabled++;
1059                } else if (account.isEnabled()) {
1060                    enabled++;
1061                }
1062            }
1063        }
1064        mBuilder.setContentText(mXmppConnectionService.getString(R.string.connected_accounts, connected, enabled));
1065        final PendingIntent openIntent = createOpenConversationsIntent();
1066        if (openIntent != null) {
1067            mBuilder.setContentIntent(openIntent);
1068        }
1069        mBuilder.setWhen(0);
1070        mBuilder.setPriority(Notification.PRIORITY_MIN);
1071        mBuilder.setSmallIcon(connected > 0 ? R.drawable.ic_link_white_24dp : R.drawable.ic_link_off_white_24dp);
1072
1073        if (Compatibility.runsTwentySix()) {
1074            mBuilder.setChannelId("foreground");
1075        }
1076
1077
1078        return mBuilder.build();
1079    }
1080
1081    private PendingIntent createOpenConversationsIntent() {
1082        try {
1083            return PendingIntent.getActivity(mXmppConnectionService, 0, new Intent(mXmppConnectionService, ConversationsActivity.class), 0);
1084        } catch (RuntimeException e) {
1085            return null;
1086        }
1087    }
1088
1089    void updateErrorNotification() {
1090        if (Config.SUPPRESS_ERROR_NOTIFICATION) {
1091            cancel(ERROR_NOTIFICATION_ID);
1092            return;
1093        }
1094        final boolean showAllErrors = QuickConversationsService.isConversations();
1095        final List<Account> errors = new ArrayList<>();
1096        boolean torNotAvailable = false;
1097        for (final Account account : mXmppConnectionService.getAccounts()) {
1098            if (account.hasErrorStatus() && account.showErrorNotification() && (showAllErrors || account.getLastErrorStatus() == Account.State.UNAUTHORIZED)) {
1099                errors.add(account);
1100                torNotAvailable |= account.getStatus() == Account.State.TOR_NOT_AVAILABLE;
1101            }
1102        }
1103        if (mXmppConnectionService.foregroundNotificationNeedsUpdatingWhenErrorStateChanges()) {
1104            notify(FOREGROUND_NOTIFICATION_ID, createForegroundNotification());
1105        }
1106        final Notification.Builder mBuilder = new Notification.Builder(mXmppConnectionService);
1107        if (errors.size() == 0) {
1108            cancel(ERROR_NOTIFICATION_ID);
1109            return;
1110        } else if (errors.size() == 1) {
1111            mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.problem_connecting_to_account));
1112            mBuilder.setContentText(errors.get(0).getJid().asBareJid().toEscapedString());
1113        } else {
1114            mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.problem_connecting_to_accounts));
1115            mBuilder.setContentText(mXmppConnectionService.getString(R.string.touch_to_fix));
1116        }
1117        mBuilder.addAction(R.drawable.ic_autorenew_white_24dp,
1118                mXmppConnectionService.getString(R.string.try_again),
1119                createTryAgainIntent()
1120        );
1121        if (torNotAvailable) {
1122            if (TorServiceUtils.isOrbotInstalled(mXmppConnectionService)) {
1123                mBuilder.addAction(
1124                        R.drawable.ic_play_circle_filled_white_48dp,
1125                        mXmppConnectionService.getString(R.string.start_orbot),
1126                        PendingIntent.getActivity(mXmppConnectionService, 147, TorServiceUtils.LAUNCH_INTENT, 0)
1127                );
1128            } else {
1129                mBuilder.addAction(
1130                        R.drawable.ic_file_download_white_24dp,
1131                        mXmppConnectionService.getString(R.string.install_orbot),
1132                        PendingIntent.getActivity(mXmppConnectionService, 146, TorServiceUtils.INSTALL_INTENT, 0)
1133                );
1134            }
1135        }
1136        mBuilder.setDeleteIntent(createDismissErrorIntent());
1137        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
1138            mBuilder.setVisibility(Notification.VISIBILITY_PRIVATE);
1139            mBuilder.setSmallIcon(R.drawable.ic_warning_white_24dp);
1140        } else {
1141            mBuilder.setSmallIcon(R.drawable.ic_stat_alert_warning);
1142        }
1143        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) {
1144            mBuilder.setLocalOnly(true);
1145        }
1146        mBuilder.setPriority(Notification.PRIORITY_LOW);
1147        final Intent intent;
1148        if (AccountUtils.MANAGE_ACCOUNT_ACTIVITY != null) {
1149            intent = new Intent(mXmppConnectionService, AccountUtils.MANAGE_ACCOUNT_ACTIVITY);
1150        } else {
1151            intent = new Intent(mXmppConnectionService, EditAccountActivity.class);
1152            intent.putExtra("jid", errors.get(0).getJid().asBareJid().toEscapedString());
1153            intent.putExtra(EditAccountActivity.EXTRA_OPENED_FROM_NOTIFICATION, true);
1154        }
1155        mBuilder.setContentIntent(PendingIntent.getActivity(mXmppConnectionService, 145, intent, PendingIntent.FLAG_UPDATE_CURRENT));
1156        if (Compatibility.runsTwentySix()) {
1157            mBuilder.setChannelId("error");
1158        }
1159        notify(ERROR_NOTIFICATION_ID, mBuilder.build());
1160    }
1161
1162    void updateFileAddingNotification(int current, Message message) {
1163        Notification.Builder mBuilder = new Notification.Builder(mXmppConnectionService);
1164        mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.transcoding_video));
1165        mBuilder.setProgress(100, current, false);
1166        mBuilder.setSmallIcon(R.drawable.ic_hourglass_empty_white_24dp);
1167        mBuilder.setContentIntent(createContentIntent(message.getConversation()));
1168        mBuilder.setOngoing(true);
1169        if (Compatibility.runsTwentySix()) {
1170            mBuilder.setChannelId("compression");
1171        }
1172        Notification notification = mBuilder.build();
1173        notify(FOREGROUND_NOTIFICATION_ID, notification);
1174    }
1175
1176    private void notify(String tag, int id, Notification notification) {
1177        final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(mXmppConnectionService);
1178        try {
1179            notificationManager.notify(tag, id, notification);
1180        } catch (RuntimeException e) {
1181            Log.d(Config.LOGTAG, "unable to make notification", e);
1182        }
1183    }
1184
1185    public void notify(int id, Notification notification) {
1186        final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(mXmppConnectionService);
1187        try {
1188            notificationManager.notify(id, notification);
1189        } catch (RuntimeException e) {
1190            Log.d(Config.LOGTAG, "unable to make notification", e);
1191        }
1192    }
1193
1194    public void cancel(int id) {
1195        final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(mXmppConnectionService);
1196        try {
1197            notificationManager.cancel(id);
1198        } catch (RuntimeException e) {
1199            Log.d(Config.LOGTAG, "unable to cancel notification", e);
1200        }
1201    }
1202
1203    private void cancel(String tag, int id) {
1204        final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(mXmppConnectionService);
1205        try {
1206            notificationManager.cancel(tag, id);
1207        } catch (RuntimeException e) {
1208            Log.d(Config.LOGTAG, "unable to cancel notification", e);
1209        }
1210    }
1211}