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