NotificationService.java

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