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