NotificationService.java

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