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 = mXmppConnectionService.getSystemService(NotificationManager.class);
 506        final int currentInterruptionFilter;
 507        if (notificationManager != null) {
 508            currentInterruptionFilter = notificationManager.getCurrentInterruptionFilter();
 509        } else {
 510            currentInterruptionFilter = 1; // INTERRUPTION_FILTER_ALL
 511        }
 512        if (currentInterruptionFilter != 1) {
 513            Log.d(
 514                    Config.LOGTAG,
 515                    "do not ring or vibrate because interruption filter has been set to "
 516                            + currentInterruptionFilter);
 517            return;
 518        }
 519        final ScheduledFuture<?> currentVibrationFuture = this.vibrationFuture;
 520        this.vibrationFuture =
 521                SCHEDULED_EXECUTOR_SERVICE.scheduleAtFixedRate(
 522                        new VibrationRunnable(), 0, 3, TimeUnit.SECONDS);
 523        if (currentVibrationFuture != null) {
 524            currentVibrationFuture.cancel(true);
 525        }
 526        final var preexistingRingtone = this.currentlyPlayingRingtone;
 527        if (preexistingRingtone != null) {
 528            preexistingRingtone.stop();
 529        }
 530        final SharedPreferences preferences =
 531                PreferenceManager.getDefaultSharedPreferences(mXmppConnectionService);
 532        final Resources resources = mXmppConnectionService.getResources();
 533        final String ringtonePreference =
 534                preferences.getString(
 535                        "call_ringtone", resources.getString(R.string.incoming_call_ringtone));
 536        if (Strings.isNullOrEmpty(ringtonePreference)) {
 537            Log.d(Config.LOGTAG, "ringtone has been set to none");
 538            return;
 539        }
 540        final Uri uri = Uri.parse(ringtonePreference);
 541        this.currentlyPlayingRingtone = RingtoneManager.getRingtone(mXmppConnectionService, uri);
 542        if (this.currentlyPlayingRingtone == null) {
 543            Log.d(Config.LOGTAG, "unable to find ringtone for uri " + uri);
 544            return;
 545        }
 546        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
 547            this.currentlyPlayingRingtone.setLooping(true);
 548        }
 549        this.currentlyPlayingRingtone.play();
 550    }
 551
 552    private void showIncomingCallNotification(
 553            final AbstractJingleConnection.Id id, final Set<Media> media) {
 554        final Intent fullScreenIntent =
 555                new Intent(mXmppConnectionService, RtpSessionActivity.class);
 556        fullScreenIntent.putExtra(
 557                RtpSessionActivity.EXTRA_ACCOUNT,
 558                id.account.getJid().asBareJid().toEscapedString());
 559        fullScreenIntent.putExtra(RtpSessionActivity.EXTRA_WITH, id.with.toEscapedString());
 560        fullScreenIntent.putExtra(RtpSessionActivity.EXTRA_SESSION_ID, id.sessionId);
 561        fullScreenIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
 562        fullScreenIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
 563        final NotificationCompat.Builder builder =
 564                new NotificationCompat.Builder(
 565                        mXmppConnectionService, INCOMING_CALLS_NOTIFICATION_CHANNEL);
 566        if (media.contains(Media.VIDEO)) {
 567            builder.setSmallIcon(R.drawable.ic_videocam_white_24dp);
 568            builder.setContentTitle(
 569                    mXmppConnectionService.getString(R.string.rtp_state_incoming_video_call));
 570        } else {
 571            builder.setSmallIcon(R.drawable.ic_call_white_24dp);
 572            builder.setContentTitle(
 573                    mXmppConnectionService.getString(R.string.rtp_state_incoming_call));
 574        }
 575        final Contact contact = id.getContact();
 576        builder.setLargeIcon(
 577                mXmppConnectionService
 578                        .getAvatarService()
 579                        .get(contact, AvatarService.getSystemUiAvatarSize(mXmppConnectionService)));
 580        final Uri systemAccount = contact.getSystemAccount();
 581        if (systemAccount != null) {
 582            builder.addPerson(systemAccount.toString());
 583        }
 584        builder.setContentText(id.account.getRoster().getContact(id.with).getDisplayName());
 585        builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC);
 586        builder.setPriority(NotificationCompat.PRIORITY_HIGH);
 587        builder.setCategory(NotificationCompat.CATEGORY_CALL);
 588        PendingIntent pendingIntent = createPendingRtpSession(id, Intent.ACTION_VIEW, 101);
 589        builder.setFullScreenIntent(pendingIntent, true);
 590        builder.setContentIntent(pendingIntent); // old androids need this?
 591        builder.setOngoing(true);
 592        builder.addAction(
 593                new NotificationCompat.Action.Builder(
 594                                R.drawable.ic_call_end_white_48dp,
 595                                mXmppConnectionService.getString(R.string.dismiss_call),
 596                                createCallAction(
 597                                        id.sessionId,
 598                                        XmppConnectionService.ACTION_DISMISS_CALL,
 599                                        102))
 600                        .build());
 601        builder.addAction(
 602                new NotificationCompat.Action.Builder(
 603                                R.drawable.ic_call_white_24dp,
 604                                mXmppConnectionService.getString(R.string.answer_call),
 605                                createPendingRtpSession(
 606                                        id, RtpSessionActivity.ACTION_ACCEPT_CALL, 103))
 607                        .build());
 608        modifyIncomingCall(builder);
 609        final Notification notification = builder.build();
 610        notification.flags = notification.flags | Notification.FLAG_INSISTENT;
 611        notify(INCOMING_CALL_NOTIFICATION_ID, notification);
 612    }
 613
 614    public Notification getOngoingCallNotification(
 615            final XmppConnectionService.OngoingCall ongoingCall) {
 616        final AbstractJingleConnection.Id id = ongoingCall.id;
 617        final NotificationCompat.Builder builder =
 618                new NotificationCompat.Builder(mXmppConnectionService, "ongoing_calls");
 619        if (ongoingCall.media.contains(Media.VIDEO)) {
 620            builder.setSmallIcon(R.drawable.ic_videocam_white_24dp);
 621            if (ongoingCall.reconnecting) {
 622                builder.setContentTitle(
 623                        mXmppConnectionService.getString(R.string.reconnecting_video_call));
 624            } else {
 625                builder.setContentTitle(
 626                        mXmppConnectionService.getString(R.string.ongoing_video_call));
 627            }
 628        } else {
 629            builder.setSmallIcon(R.drawable.ic_call_white_24dp);
 630            if (ongoingCall.reconnecting) {
 631                builder.setContentTitle(
 632                        mXmppConnectionService.getString(R.string.reconnecting_call));
 633            } else {
 634                builder.setContentTitle(mXmppConnectionService.getString(R.string.ongoing_call));
 635            }
 636        }
 637        builder.setContentText(id.account.getRoster().getContact(id.with).getDisplayName());
 638        builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC);
 639        builder.setPriority(NotificationCompat.PRIORITY_HIGH);
 640        builder.setCategory(NotificationCompat.CATEGORY_CALL);
 641        builder.setContentIntent(createPendingRtpSession(id, Intent.ACTION_VIEW, 101));
 642        builder.setOngoing(true);
 643        builder.addAction(
 644                new NotificationCompat.Action.Builder(
 645                                R.drawable.ic_call_end_white_48dp,
 646                                mXmppConnectionService.getString(R.string.hang_up),
 647                                createCallAction(
 648                                        id.sessionId, XmppConnectionService.ACTION_END_CALL, 104))
 649                        .build());
 650        builder.setLocalOnly(true);
 651        return builder.build();
 652    }
 653
 654    private PendingIntent createPendingRtpSession(
 655            final AbstractJingleConnection.Id id, final String action, final int requestCode) {
 656        final Intent fullScreenIntent =
 657                new Intent(mXmppConnectionService, RtpSessionActivity.class);
 658        fullScreenIntent.setAction(action);
 659        fullScreenIntent.putExtra(
 660                RtpSessionActivity.EXTRA_ACCOUNT,
 661                id.account.getJid().asBareJid().toEscapedString());
 662        fullScreenIntent.putExtra(RtpSessionActivity.EXTRA_WITH, id.with.toEscapedString());
 663        fullScreenIntent.putExtra(RtpSessionActivity.EXTRA_SESSION_ID, id.sessionId);
 664        return PendingIntent.getActivity(
 665                mXmppConnectionService,
 666                requestCode,
 667                fullScreenIntent,
 668                s()
 669                        ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
 670                        : PendingIntent.FLAG_UPDATE_CURRENT);
 671    }
 672
 673    public void cancelIncomingCallNotification() {
 674        stopSoundAndVibration();
 675        cancel(INCOMING_CALL_NOTIFICATION_ID);
 676    }
 677
 678    public boolean stopSoundAndVibration() {
 679        int stopped = 0;
 680        if (this.currentlyPlayingRingtone != null) {
 681            if (this.currentlyPlayingRingtone.isPlaying()) {
 682                Log.d(Config.LOGTAG, "stop playing ring tone");
 683                ++stopped;
 684            }
 685            this.currentlyPlayingRingtone.stop();
 686        }
 687        if (this.vibrationFuture != null && !this.vibrationFuture.isCancelled()) {
 688            Log.d(Config.LOGTAG, "stop vibration");
 689            this.vibrationFuture.cancel(true);
 690            ++stopped;
 691        }
 692        return stopped > 0;
 693    }
 694
 695    public static void cancelIncomingCallNotification(final Context context) {
 696        final NotificationManagerCompat notificationManager =
 697                NotificationManagerCompat.from(context);
 698        try {
 699            notificationManager.cancel(INCOMING_CALL_NOTIFICATION_ID);
 700        } catch (RuntimeException e) {
 701            Log.d(Config.LOGTAG, "unable to cancel incoming call notification after crash", e);
 702        }
 703    }
 704
 705    private void pushNow(final Message message) {
 706        mXmppConnectionService.updateUnreadCountBadge();
 707        if (!notifyMessage(message)) {
 708            Log.d(
 709                    Config.LOGTAG,
 710                    message.getConversation().getAccount().getJid().asBareJid()
 711                            + ": suppressing notification because turned off");
 712            return;
 713        }
 714        final boolean isScreenLocked = mXmppConnectionService.isScreenLocked();
 715        if (this.mIsInForeground
 716                && !isScreenLocked
 717                && this.mOpenConversation == message.getConversation()) {
 718            Log.d(
 719                    Config.LOGTAG,
 720                    message.getConversation().getAccount().getJid().asBareJid()
 721                            + ": suppressing notification because conversation is open");
 722            return;
 723        }
 724        synchronized (notifications) {
 725            pushToStack(message);
 726            final Conversational conversation = message.getConversation();
 727            final Account account = conversation.getAccount();
 728            final boolean doNotify =
 729                    (!(this.mIsInForeground && this.mOpenConversation == null) || isScreenLocked)
 730                            && !account.inGracePeriod()
 731                            && !this.inMiniGracePeriod(account);
 732            updateNotification(doNotify, Collections.singletonList(conversation.getUuid()));
 733        }
 734    }
 735
 736    private void pushMissedCall(final Message message) {
 737        final Conversational conversation = message.getConversation();
 738        final MissedCallsInfo info = mMissedCalls.get(conversation);
 739        if (info == null) {
 740            mMissedCalls.put(conversation, new MissedCallsInfo(message.getTimeSent()));
 741        } else {
 742            info.newMissedCall(message.getTimeSent());
 743        }
 744    }
 745
 746    public void pushMissedCallNow(final Message message) {
 747        synchronized (mMissedCalls) {
 748            pushMissedCall(message);
 749            updateMissedCallNotifications(Collections.singleton(message.getConversation()));
 750        }
 751    }
 752
 753    public void clear(final Conversation conversation) {
 754        clearMessages(conversation);
 755        clearMissedCalls(conversation);
 756    }
 757
 758    public void clearMessages() {
 759        synchronized (notifications) {
 760            for (ArrayList<Message> messages : notifications.values()) {
 761                markAsReadIfHasDirectReply(messages);
 762            }
 763            notifications.clear();
 764            updateNotification(false);
 765        }
 766    }
 767
 768    public void clearMessages(final Conversation conversation) {
 769        synchronized (this.mBacklogMessageCounter) {
 770            this.mBacklogMessageCounter.remove(conversation);
 771        }
 772        synchronized (notifications) {
 773            markAsReadIfHasDirectReply(conversation);
 774            if (notifications.remove(conversation.getUuid()) != null) {
 775                cancel(conversation.getUuid(), NOTIFICATION_ID);
 776                updateNotification(false, null, true);
 777            }
 778        }
 779    }
 780
 781    public void clearMissedCall(final Message message) {
 782        synchronized (mMissedCalls) {
 783            final Iterator<Map.Entry<Conversational,MissedCallsInfo>> iterator = mMissedCalls.entrySet().iterator();
 784            while (iterator.hasNext()) {
 785                final Map.Entry<Conversational, MissedCallsInfo> entry = iterator.next();
 786                final Conversational conversational = entry.getKey();
 787                final MissedCallsInfo missedCallsInfo = entry.getValue();
 788                if (conversational.getUuid().equals(message.getConversation().getUuid())) {
 789                    if (missedCallsInfo.removeMissedCall()) {
 790                        cancel(conversational.getUuid(), MISSED_CALL_NOTIFICATION_ID);
 791                        Log.d(Config.LOGTAG,conversational.getAccount().getJid().asBareJid()+": dismissed missed call because call was picked up on other device");
 792                        iterator.remove();
 793                    }
 794                }
 795            }
 796            updateMissedCallNotifications(null);
 797        }
 798    }
 799
 800    public void clearMissedCalls() {
 801        synchronized (mMissedCalls) {
 802            for (final Conversational conversation : mMissedCalls.keySet()) {
 803                cancel(conversation.getUuid(), MISSED_CALL_NOTIFICATION_ID);
 804            }
 805            mMissedCalls.clear();
 806            updateMissedCallNotifications(null);
 807        }
 808    }
 809
 810    public void clearMissedCalls(final Conversation conversation) {
 811        synchronized (mMissedCalls) {
 812            if (mMissedCalls.remove(conversation) != null) {
 813                cancel(conversation.getUuid(), MISSED_CALL_NOTIFICATION_ID);
 814                updateMissedCallNotifications(null);
 815            }
 816        }
 817    }
 818
 819    private void markAsReadIfHasDirectReply(final Conversation conversation) {
 820        markAsReadIfHasDirectReply(notifications.get(conversation.getUuid()));
 821    }
 822
 823    private void markAsReadIfHasDirectReply(final ArrayList<Message> messages) {
 824        if (messages != null && messages.size() > 0) {
 825            Message last = messages.get(messages.size() - 1);
 826            if (last.getStatus() != Message.STATUS_RECEIVED) {
 827                if (mXmppConnectionService.markRead((Conversation) last.getConversation(), false)) {
 828                    mXmppConnectionService.updateConversationUi();
 829                }
 830            }
 831        }
 832    }
 833
 834    private void setNotificationColor(final Builder mBuilder) {
 835        mBuilder.setColor(ContextCompat.getColor(mXmppConnectionService, R.color.green600));
 836    }
 837
 838    public void updateNotification() {
 839        synchronized (notifications) {
 840            updateNotification(false);
 841        }
 842    }
 843
 844    private void updateNotification(final boolean notify) {
 845        updateNotification(notify, null, false);
 846    }
 847
 848    private void updateNotification(final boolean notify, final List<String> conversations) {
 849        updateNotification(notify, conversations, false);
 850    }
 851
 852    private void updateNotification(
 853            final boolean notify, final List<String> conversations, final boolean summaryOnly) {
 854        final SharedPreferences preferences =
 855                PreferenceManager.getDefaultSharedPreferences(mXmppConnectionService);
 856
 857        final boolean quiteHours = isQuietHours();
 858
 859        final boolean notifyOnlyOneChild =
 860                notify
 861                        && conversations != null
 862                        && conversations.size()
 863                                == 1; // if this check is changed to > 0 catchup messages will
 864        // create one notification per conversation
 865
 866        if (notifications.size() == 0) {
 867            cancel(NOTIFICATION_ID);
 868        } else {
 869            if (notify) {
 870                this.markLastNotification();
 871            }
 872            final Builder mBuilder;
 873            if (notifications.size() == 1 && Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
 874                mBuilder =
 875                        buildSingleConversations(
 876                                notifications.values().iterator().next(), notify, quiteHours);
 877                modifyForSoundVibrationAndLight(mBuilder, notify, quiteHours, preferences);
 878                notify(NOTIFICATION_ID, mBuilder.build());
 879            } else {
 880                mBuilder = buildMultipleConversation(notify, quiteHours);
 881                if (notifyOnlyOneChild) {
 882                    mBuilder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN);
 883                }
 884                modifyForSoundVibrationAndLight(mBuilder, notify, quiteHours, preferences);
 885                if (!summaryOnly) {
 886                    for (Map.Entry<String, ArrayList<Message>> entry : notifications.entrySet()) {
 887                        String uuid = entry.getKey();
 888                        final boolean notifyThis =
 889                                notifyOnlyOneChild ? conversations.contains(uuid) : notify;
 890                        Builder singleBuilder =
 891                                buildSingleConversations(entry.getValue(), notifyThis, quiteHours);
 892                        if (!notifyOnlyOneChild) {
 893                            singleBuilder.setGroupAlertBehavior(
 894                                    NotificationCompat.GROUP_ALERT_SUMMARY);
 895                        }
 896                        modifyForSoundVibrationAndLight(
 897                                singleBuilder, notifyThis, quiteHours, preferences);
 898                        singleBuilder.setGroup(MESSAGES_GROUP);
 899                        setNotificationColor(singleBuilder);
 900                        notify(entry.getKey(), NOTIFICATION_ID, singleBuilder.build());
 901                    }
 902                }
 903                notify(NOTIFICATION_ID, mBuilder.build());
 904            }
 905        }
 906    }
 907
 908    private void updateMissedCallNotifications(final Set<Conversational> update) {
 909        if (mMissedCalls.isEmpty()) {
 910            cancel(MISSED_CALL_NOTIFICATION_ID);
 911            return;
 912        }
 913        if (mMissedCalls.size() == 1 && Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
 914            final Conversational conversation = mMissedCalls.keySet().iterator().next();
 915            final MissedCallsInfo info = mMissedCalls.values().iterator().next();
 916            final Notification notification = missedCall(conversation, info);
 917            notify(MISSED_CALL_NOTIFICATION_ID, notification);
 918        } else {
 919            final Notification summary = missedCallsSummary();
 920            notify(MISSED_CALL_NOTIFICATION_ID, summary);
 921            if (update != null) {
 922                for (final Conversational conversation : update) {
 923                    final MissedCallsInfo info = mMissedCalls.get(conversation);
 924                    if (info != null) {
 925                        final Notification notification = missedCall(conversation, info);
 926                        notify(conversation.getUuid(), MISSED_CALL_NOTIFICATION_ID, notification);
 927                    }
 928                }
 929            }
 930        }
 931    }
 932
 933    private void modifyForSoundVibrationAndLight(
 934            Builder mBuilder, boolean notify, boolean quietHours, SharedPreferences preferences) {
 935        final Resources resources = mXmppConnectionService.getResources();
 936        final String ringtone =
 937                preferences.getString(
 938                        "notification_ringtone",
 939                        resources.getString(R.string.notification_ringtone));
 940        final boolean vibrate =
 941                preferences.getBoolean(
 942                        "vibrate_on_notification",
 943                        resources.getBoolean(R.bool.vibrate_on_notification));
 944        final boolean led = preferences.getBoolean("led", resources.getBoolean(R.bool.led));
 945        final boolean headsup =
 946                preferences.getBoolean(
 947                        "notification_headsup", resources.getBoolean(R.bool.headsup_notifications));
 948        if (notify && !quietHours) {
 949            if (vibrate) {
 950                final int dat = 70;
 951                final long[] pattern = {0, 3 * dat, dat, dat};
 952                mBuilder.setVibrate(pattern);
 953            } else {
 954                mBuilder.setVibrate(new long[] {0});
 955            }
 956            Uri uri = Uri.parse(ringtone);
 957            try {
 958                mBuilder.setSound(fixRingtoneUri(uri));
 959            } catch (SecurityException e) {
 960                Log.d(Config.LOGTAG, "unable to use custom notification sound " + uri.toString());
 961            }
 962        } else {
 963            mBuilder.setLocalOnly(true);
 964        }
 965        mBuilder.setCategory(Notification.CATEGORY_MESSAGE);
 966        mBuilder.setPriority(
 967                notify
 968                        ? (headsup
 969                                ? NotificationCompat.PRIORITY_HIGH
 970                                : NotificationCompat.PRIORITY_DEFAULT)
 971                        : NotificationCompat.PRIORITY_LOW);
 972        setNotificationColor(mBuilder);
 973        mBuilder.setDefaults(0);
 974        if (led) {
 975            mBuilder.setLights(LED_COLOR, 2000, 3000);
 976        }
 977    }
 978
 979    private void modifyIncomingCall(final Builder mBuilder) {
 980        mBuilder.setPriority(NotificationCompat.PRIORITY_HIGH);
 981        setNotificationColor(mBuilder);
 982        mBuilder.setLights(LED_COLOR, 2000, 3000);
 983    }
 984
 985    private Uri fixRingtoneUri(Uri uri) {
 986        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && "file".equals(uri.getScheme())) {
 987            return FileBackend.getUriForFile(mXmppConnectionService, new File(uri.getPath()));
 988        } else {
 989            return uri;
 990        }
 991    }
 992
 993    private Notification missedCallsSummary() {
 994        final Builder publicBuilder = buildMissedCallsSummary(true);
 995        final Builder builder = buildMissedCallsSummary(false);
 996        builder.setPublicVersion(publicBuilder.build());
 997        return builder.build();
 998    }
 999
1000    private Builder buildMissedCallsSummary(boolean publicVersion) {
1001        final Builder builder =
1002                new NotificationCompat.Builder(mXmppConnectionService, "missed_calls");
1003        int totalCalls = 0;
1004        final List<String> names = new ArrayList<>();
1005        long lastTime = 0;
1006        for (final Map.Entry<Conversational, MissedCallsInfo> entry : mMissedCalls.entrySet()) {
1007            final Conversational conversation = entry.getKey();
1008            final MissedCallsInfo missedCallsInfo = entry.getValue();
1009            names.add(conversation.getContact().getDisplayName());
1010            totalCalls += missedCallsInfo.getNumberOfCalls();
1011            lastTime = Math.max(lastTime, missedCallsInfo.getLastTime());
1012        }
1013        final String title =
1014                (totalCalls == 1)
1015                        ? mXmppConnectionService.getString(R.string.missed_call)
1016                        : (mMissedCalls.size() == 1)
1017                                ? mXmppConnectionService
1018                                        .getResources()
1019                                        .getQuantityString(
1020                                                R.plurals.n_missed_calls, totalCalls, totalCalls)
1021                                : mXmppConnectionService
1022                                        .getResources()
1023                                        .getQuantityString(
1024                                                R.plurals.n_missed_calls_from_m_contacts,
1025                                                mMissedCalls.size(),
1026                                                totalCalls,
1027                                                mMissedCalls.size());
1028        builder.setContentTitle(title);
1029        builder.setTicker(title);
1030        if (!publicVersion) {
1031            builder.setContentText(Joiner.on(", ").join(names));
1032        }
1033        builder.setSmallIcon(R.drawable.ic_call_missed_white_24db);
1034        builder.setGroupSummary(true);
1035        builder.setGroup(MISSED_CALLS_GROUP);
1036        builder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN);
1037        builder.setCategory(NotificationCompat.CATEGORY_CALL);
1038        builder.setWhen(lastTime);
1039        if (!mMissedCalls.isEmpty()) {
1040            final Conversational firstConversation = mMissedCalls.keySet().iterator().next();
1041            builder.setContentIntent(createContentIntent(firstConversation));
1042        }
1043        builder.setDeleteIntent(createMissedCallsDeleteIntent(null));
1044        modifyMissedCall(builder);
1045        return builder;
1046    }
1047
1048    private Notification missedCall(final Conversational conversation, final MissedCallsInfo info) {
1049        final Builder publicBuilder = buildMissedCall(conversation, info, true);
1050        final Builder builder = buildMissedCall(conversation, info, false);
1051        builder.setPublicVersion(publicBuilder.build());
1052        return builder.build();
1053    }
1054
1055    private Builder buildMissedCall(
1056            final Conversational conversation, final MissedCallsInfo info, boolean publicVersion) {
1057        final Builder builder =
1058                new NotificationCompat.Builder(mXmppConnectionService, "missed_calls");
1059        final String title =
1060                (info.getNumberOfCalls() == 1)
1061                        ? mXmppConnectionService.getString(R.string.missed_call)
1062                        : mXmppConnectionService
1063                                .getResources()
1064                                .getQuantityString(
1065                                        R.plurals.n_missed_calls,
1066                                        info.getNumberOfCalls(),
1067                                        info.getNumberOfCalls());
1068        builder.setContentTitle(title);
1069        final String name = conversation.getContact().getDisplayName();
1070        if (publicVersion) {
1071            builder.setTicker(title);
1072        } else {
1073            builder.setTicker(
1074                    mXmppConnectionService
1075                            .getResources()
1076                            .getQuantityString(
1077                                    R.plurals.n_missed_calls_from_x,
1078                                    info.getNumberOfCalls(),
1079                                    info.getNumberOfCalls(),
1080                                    name));
1081            builder.setContentText(name);
1082        }
1083        builder.setSmallIcon(R.drawable.ic_call_missed_white_24db);
1084        builder.setGroup(MISSED_CALLS_GROUP);
1085        builder.setCategory(NotificationCompat.CATEGORY_CALL);
1086        builder.setWhen(info.getLastTime());
1087        builder.setContentIntent(createContentIntent(conversation));
1088        builder.setDeleteIntent(createMissedCallsDeleteIntent(conversation));
1089        if (!publicVersion && conversation instanceof Conversation) {
1090            builder.setLargeIcon(
1091                    mXmppConnectionService
1092                            .getAvatarService()
1093                            .get(
1094                                    (Conversation) conversation,
1095                                    AvatarService.getSystemUiAvatarSize(mXmppConnectionService)));
1096        }
1097        modifyMissedCall(builder);
1098        return builder;
1099    }
1100
1101    private void modifyMissedCall(final Builder builder) {
1102        final SharedPreferences preferences =
1103                PreferenceManager.getDefaultSharedPreferences(mXmppConnectionService);
1104        final Resources resources = mXmppConnectionService.getResources();
1105        final boolean led = preferences.getBoolean("led", resources.getBoolean(R.bool.led));
1106        if (led) {
1107            builder.setLights(LED_COLOR, 2000, 3000);
1108        }
1109        builder.setPriority(NotificationCompat.PRIORITY_HIGH);
1110        builder.setSound(null);
1111        setNotificationColor(builder);
1112    }
1113
1114    private Builder buildMultipleConversation(final boolean notify, final boolean quietHours) {
1115        final Builder mBuilder =
1116                new NotificationCompat.Builder(
1117                        mXmppConnectionService,
1118                        quietHours ? "quiet_hours" : (notify ? MESSAGES_NOTIFICATION_CHANNEL : "silent_messages"));
1119        final NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle();
1120        style.setBigContentTitle(
1121                mXmppConnectionService
1122                        .getResources()
1123                        .getQuantityString(
1124                                R.plurals.x_unread_conversations,
1125                                notifications.size(),
1126                                notifications.size()));
1127        final List<String> names = new ArrayList<>();
1128        Conversation conversation = null;
1129        for (final ArrayList<Message> messages : notifications.values()) {
1130            if (messages.isEmpty()) {
1131                continue;
1132            }
1133            conversation = (Conversation) messages.get(0).getConversation();
1134            final String name = conversation.getName().toString();
1135            SpannableString styledString;
1136            if (Config.HIDE_MESSAGE_TEXT_IN_NOTIFICATION) {
1137                int count = messages.size();
1138                styledString =
1139                        new SpannableString(
1140                                name
1141                                        + ": "
1142                                        + mXmppConnectionService
1143                                                .getResources()
1144                                                .getQuantityString(
1145                                                        R.plurals.x_messages, count, count));
1146                styledString.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), 0);
1147                style.addLine(styledString);
1148            } else {
1149                styledString =
1150                        new SpannableString(
1151                                name
1152                                        + ": "
1153                                        + UIHelper.getMessagePreview(
1154                                                        mXmppConnectionService, messages.get(0))
1155                                                .first);
1156                styledString.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), 0);
1157                style.addLine(styledString);
1158            }
1159            names.add(name);
1160        }
1161        final String contentTitle =
1162                mXmppConnectionService
1163                        .getResources()
1164                        .getQuantityString(
1165                                R.plurals.x_unread_conversations,
1166                                notifications.size(),
1167                                notifications.size());
1168        mBuilder.setContentTitle(contentTitle);
1169        mBuilder.setTicker(contentTitle);
1170        mBuilder.setContentText(Joiner.on(", ").join(names));
1171        mBuilder.setStyle(style);
1172        if (conversation != null) {
1173            mBuilder.setContentIntent(createContentIntent(conversation));
1174        }
1175        mBuilder.setGroupSummary(true);
1176        mBuilder.setGroup(MESSAGES_GROUP);
1177        mBuilder.setDeleteIntent(createDeleteIntent(null));
1178        mBuilder.setSmallIcon(R.drawable.ic_notification);
1179        return mBuilder;
1180    }
1181
1182    private Builder buildSingleConversations(
1183            final ArrayList<Message> messages, final boolean notify, final boolean quietHours) {
1184        final var channel = quietHours ? "quiet_hours" : (notify ? MESSAGES_NOTIFICATION_CHANNEL : "silent_messages");
1185        final Builder notificationBuilder =
1186                new NotificationCompat.Builder(mXmppConnectionService, channel);
1187        if (messages.isEmpty()) {
1188            return notificationBuilder;
1189        }
1190        final Conversation conversation = (Conversation) messages.get(0).getConversation();
1191        notificationBuilder.setLargeIcon(
1192                mXmppConnectionService
1193                        .getAvatarService()
1194                        .get(
1195                                conversation,
1196                                AvatarService.getSystemUiAvatarSize(mXmppConnectionService)));
1197        notificationBuilder.setContentTitle(conversation.getName());
1198        if (Config.HIDE_MESSAGE_TEXT_IN_NOTIFICATION) {
1199            int count = messages.size();
1200            notificationBuilder.setContentText(
1201                    mXmppConnectionService
1202                            .getResources()
1203                            .getQuantityString(R.plurals.x_messages, count, count));
1204        } else {
1205            final Message message;
1206            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P
1207                    && (message = getImage(messages)) != null) {
1208                modifyForImage(notificationBuilder, message, messages);
1209            } else {
1210                modifyForTextOnly(notificationBuilder, messages);
1211            }
1212            RemoteInput remoteInput =
1213                    new RemoteInput.Builder("text_reply")
1214                            .setLabel(UIHelper.getMessageHint(mXmppConnectionService, conversation))
1215                            .build();
1216            PendingIntent markAsReadPendingIntent = createReadPendingIntent(conversation);
1217            NotificationCompat.Action markReadAction =
1218                    new NotificationCompat.Action.Builder(
1219                                    R.drawable.ic_drafts_white_24dp,
1220                                    mXmppConnectionService.getString(R.string.mark_as_read),
1221                                    markAsReadPendingIntent)
1222                            .setSemanticAction(
1223                                    NotificationCompat.Action.SEMANTIC_ACTION_MARK_AS_READ)
1224                            .setShowsUserInterface(false)
1225                            .build();
1226            final String replyLabel = mXmppConnectionService.getString(R.string.reply);
1227            final String lastMessageUuid = Iterables.getLast(messages).getUuid();
1228            final NotificationCompat.Action replyAction =
1229                    new NotificationCompat.Action.Builder(
1230                                    R.drawable.ic_send_text_offline,
1231                                    replyLabel,
1232                                    createReplyIntent(conversation, lastMessageUuid, false))
1233                            .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY)
1234                            .setShowsUserInterface(false)
1235                            .addRemoteInput(remoteInput)
1236                            .build();
1237            final NotificationCompat.Action wearReplyAction =
1238                    new NotificationCompat.Action.Builder(
1239                                    R.drawable.ic_wear_reply,
1240                                    replyLabel,
1241                                    createReplyIntent(conversation, lastMessageUuid, true))
1242                            .addRemoteInput(remoteInput)
1243                            .build();
1244            notificationBuilder.extend(
1245                    new NotificationCompat.WearableExtender().addAction(wearReplyAction));
1246            int addedActionsCount = 1;
1247            notificationBuilder.addAction(markReadAction);
1248            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
1249                notificationBuilder.addAction(replyAction);
1250                ++addedActionsCount;
1251            }
1252
1253            if (displaySnoozeAction(messages)) {
1254                String label = mXmppConnectionService.getString(R.string.snooze);
1255                PendingIntent pendingSnoozeIntent = createSnoozeIntent(conversation);
1256                NotificationCompat.Action snoozeAction =
1257                        new NotificationCompat.Action.Builder(
1258                                        R.drawable.ic_notifications_paused_white_24dp,
1259                                        label,
1260                                        pendingSnoozeIntent)
1261                                .build();
1262                notificationBuilder.addAction(snoozeAction);
1263                ++addedActionsCount;
1264            }
1265            if (addedActionsCount < 3) {
1266                final Message firstLocationMessage = getFirstLocationMessage(messages);
1267                if (firstLocationMessage != null) {
1268                    final PendingIntent pendingShowLocationIntent =
1269                            createShowLocationIntent(firstLocationMessage);
1270                    if (pendingShowLocationIntent != null) {
1271                        final String label =
1272                                mXmppConnectionService
1273                                        .getResources()
1274                                        .getString(R.string.show_location);
1275                        NotificationCompat.Action locationAction =
1276                                new NotificationCompat.Action.Builder(
1277                                                R.drawable.ic_room_white_24dp,
1278                                                label,
1279                                                pendingShowLocationIntent)
1280                                        .build();
1281                        notificationBuilder.addAction(locationAction);
1282                        ++addedActionsCount;
1283                    }
1284                }
1285            }
1286            if (addedActionsCount < 3) {
1287                Message firstDownloadableMessage = getFirstDownloadableMessage(messages);
1288                if (firstDownloadableMessage != null) {
1289                    String label =
1290                            mXmppConnectionService
1291                                    .getResources()
1292                                    .getString(
1293                                            R.string.download_x_file,
1294                                            UIHelper.getFileDescriptionString(
1295                                                    mXmppConnectionService,
1296                                                    firstDownloadableMessage));
1297                    PendingIntent pendingDownloadIntent =
1298                            createDownloadIntent(firstDownloadableMessage);
1299                    NotificationCompat.Action downloadAction =
1300                            new NotificationCompat.Action.Builder(
1301                                            R.drawable.ic_file_download_white_24dp,
1302                                            label,
1303                                            pendingDownloadIntent)
1304                                    .build();
1305                    notificationBuilder.addAction(downloadAction);
1306                    ++addedActionsCount;
1307                }
1308            }
1309        }
1310        final ShortcutInfoCompat info;
1311        if (conversation.getMode() == Conversation.MODE_SINGLE) {
1312            final Contact contact = conversation.getContact();
1313            final Uri systemAccount = contact.getSystemAccount();
1314            if (systemAccount != null) {
1315                notificationBuilder.addPerson(systemAccount.toString());
1316            }
1317            info = mXmppConnectionService.getShortcutService().getShortcutInfoCompat(contact);
1318        } else {
1319            info =
1320                    mXmppConnectionService
1321                            .getShortcutService()
1322                            .getShortcutInfoCompat(conversation.getMucOptions());
1323        }
1324        notificationBuilder.setWhen(conversation.getLatestMessage().getTimeSent());
1325        notificationBuilder.setSmallIcon(R.drawable.ic_notification);
1326        notificationBuilder.setDeleteIntent(createDeleteIntent(conversation));
1327        notificationBuilder.setContentIntent(createContentIntent(conversation));
1328        if (channel.equals(MESSAGES_NOTIFICATION_CHANNEL)) {
1329            // when do not want 'customized' notifications for silent notifications in their
1330            // respective channels
1331            notificationBuilder.setShortcutInfo(info);
1332            if (Build.VERSION.SDK_INT >= 30) {
1333                mXmppConnectionService
1334                        .getSystemService(ShortcutManager.class)
1335                        .pushDynamicShortcut(info.toShortcutInfo());
1336            }
1337        }
1338        return notificationBuilder;
1339    }
1340
1341    private void modifyForImage(
1342            final Builder builder, final Message message, final ArrayList<Message> messages) {
1343        try {
1344            final Bitmap bitmap =
1345                    mXmppConnectionService
1346                            .getFileBackend()
1347                            .getThumbnail(message, getPixel(288), false);
1348            final ArrayList<Message> tmp = new ArrayList<>();
1349            for (final Message msg : messages) {
1350                if (msg.getType() == Message.TYPE_TEXT && msg.getTransferable() == null) {
1351                    tmp.add(msg);
1352                }
1353            }
1354            final BigPictureStyle bigPictureStyle = new NotificationCompat.BigPictureStyle();
1355            bigPictureStyle.bigPicture(bitmap);
1356            if (tmp.size() > 0) {
1357                CharSequence text = getMergedBodies(tmp);
1358                bigPictureStyle.setSummaryText(text);
1359                builder.setContentText(text);
1360                builder.setTicker(text);
1361            } else {
1362                final String description =
1363                        UIHelper.getFileDescriptionString(mXmppConnectionService, message);
1364                builder.setContentText(description);
1365                builder.setTicker(description);
1366            }
1367            builder.setStyle(bigPictureStyle);
1368        } catch (final IOException e) {
1369            modifyForTextOnly(builder, messages);
1370        }
1371    }
1372
1373    private Person getPerson(Message message) {
1374        final Contact contact = message.getContact();
1375        final Person.Builder builder = new Person.Builder();
1376        if (contact != null) {
1377            builder.setName(contact.getDisplayName());
1378            final Uri uri = contact.getSystemAccount();
1379            if (uri != null) {
1380                builder.setUri(uri.toString());
1381            }
1382        } else {
1383            builder.setName(UIHelper.getMessageDisplayName(message));
1384        }
1385        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
1386            builder.setIcon(
1387                    IconCompat.createWithBitmap(
1388                            mXmppConnectionService
1389                                    .getAvatarService()
1390                                    .get(
1391                                            message,
1392                                            AvatarService.getSystemUiAvatarSize(
1393                                                    mXmppConnectionService),
1394                                            false)));
1395        }
1396        return builder.build();
1397    }
1398
1399    private void modifyForTextOnly(final Builder builder, final ArrayList<Message> messages) {
1400        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
1401            final Conversation conversation = (Conversation) messages.get(0).getConversation();
1402            final Person.Builder meBuilder =
1403                    new Person.Builder().setName(mXmppConnectionService.getString(R.string.me));
1404            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
1405                meBuilder.setIcon(
1406                        IconCompat.createWithBitmap(
1407                                mXmppConnectionService
1408                                        .getAvatarService()
1409                                        .get(
1410                                                conversation.getAccount(),
1411                                                AvatarService.getSystemUiAvatarSize(
1412                                                        mXmppConnectionService))));
1413            }
1414            final Person me = meBuilder.build();
1415            NotificationCompat.MessagingStyle messagingStyle =
1416                    new NotificationCompat.MessagingStyle(me);
1417            final boolean multiple = conversation.getMode() == Conversation.MODE_MULTI;
1418            if (multiple) {
1419                messagingStyle.setConversationTitle(conversation.getName());
1420            }
1421            for (Message message : messages) {
1422                final Person sender =
1423                        message.getStatus() == Message.STATUS_RECEIVED ? getPerson(message) : null;
1424                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && isImageMessage(message)) {
1425                    final Uri dataUri =
1426                            FileBackend.getMediaUri(
1427                                    mXmppConnectionService,
1428                                    mXmppConnectionService.getFileBackend().getFile(message));
1429                    NotificationCompat.MessagingStyle.Message imageMessage =
1430                            new NotificationCompat.MessagingStyle.Message(
1431                                    UIHelper.getMessagePreview(mXmppConnectionService, message)
1432                                            .first,
1433                                    message.getTimeSent(),
1434                                    sender);
1435                    if (dataUri != null) {
1436                        imageMessage.setData(message.getMimeType(), dataUri);
1437                    }
1438                    messagingStyle.addMessage(imageMessage);
1439                } else {
1440                    messagingStyle.addMessage(
1441                            UIHelper.getMessagePreview(mXmppConnectionService, message).first,
1442                            message.getTimeSent(),
1443                            sender);
1444                }
1445            }
1446            messagingStyle.setGroupConversation(multiple);
1447            builder.setStyle(messagingStyle);
1448        } else {
1449            if (messages.get(0).getConversation().getMode() == Conversation.MODE_SINGLE) {
1450                builder.setStyle(
1451                        new NotificationCompat.BigTextStyle().bigText(getMergedBodies(messages)));
1452                final CharSequence preview =
1453                        UIHelper.getMessagePreview(
1454                                        mXmppConnectionService, messages.get(messages.size() - 1))
1455                                .first;
1456                builder.setContentText(preview);
1457                builder.setTicker(preview);
1458                builder.setNumber(messages.size());
1459            } else {
1460                final NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle();
1461                SpannableString styledString;
1462                for (Message message : messages) {
1463                    final String name = UIHelper.getMessageDisplayName(message);
1464                    styledString = new SpannableString(name + ": " + message.getBody());
1465                    styledString.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), 0);
1466                    style.addLine(styledString);
1467                }
1468                builder.setStyle(style);
1469                int count = messages.size();
1470                if (count == 1) {
1471                    final String name = UIHelper.getMessageDisplayName(messages.get(0));
1472                    styledString = new SpannableString(name + ": " + messages.get(0).getBody());
1473                    styledString.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), 0);
1474                    builder.setContentText(styledString);
1475                    builder.setTicker(styledString);
1476                } else {
1477                    final String text =
1478                            mXmppConnectionService
1479                                    .getResources()
1480                                    .getQuantityString(R.plurals.x_messages, count, count);
1481                    builder.setContentText(text);
1482                    builder.setTicker(text);
1483                }
1484            }
1485        }
1486    }
1487
1488    private Message getImage(final Iterable<Message> messages) {
1489        Message image = null;
1490        for (final Message message : messages) {
1491            if (message.getStatus() != Message.STATUS_RECEIVED) {
1492                return null;
1493            }
1494            if (isImageMessage(message)) {
1495                image = message;
1496            }
1497        }
1498        return image;
1499    }
1500
1501    private Message getFirstDownloadableMessage(final Iterable<Message> messages) {
1502        for (final Message message : messages) {
1503            if (message.getTransferable() != null
1504                    || (message.getType() == Message.TYPE_TEXT && message.treatAsDownloadable())) {
1505                return message;
1506            }
1507        }
1508        return null;
1509    }
1510
1511    private Message getFirstLocationMessage(final Iterable<Message> messages) {
1512        for (final Message message : messages) {
1513            if (message.isGeoUri()) {
1514                return message;
1515            }
1516        }
1517        return null;
1518    }
1519
1520    private CharSequence getMergedBodies(final ArrayList<Message> messages) {
1521        final StringBuilder text = new StringBuilder();
1522        for (Message message : messages) {
1523            if (text.length() != 0) {
1524                text.append("\n");
1525            }
1526            text.append(UIHelper.getMessagePreview(mXmppConnectionService, message).first);
1527        }
1528        return text.toString();
1529    }
1530
1531    private PendingIntent createShowLocationIntent(final Message message) {
1532        Iterable<Intent> intents =
1533                GeoHelper.createGeoIntentsFromMessage(mXmppConnectionService, message);
1534        for (final Intent intent : intents) {
1535            if (intent.resolveActivity(mXmppConnectionService.getPackageManager()) != null) {
1536                return PendingIntent.getActivity(
1537                        mXmppConnectionService,
1538                        generateRequestCode(message.getConversation(), 18),
1539                        intent,
1540                        s()
1541                                ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1542                                : PendingIntent.FLAG_UPDATE_CURRENT);
1543            }
1544        }
1545        return null;
1546    }
1547
1548    private PendingIntent createContentIntent(
1549            final String conversationUuid, final String downloadMessageUuid) {
1550        final Intent viewConversationIntent =
1551                new Intent(mXmppConnectionService, ConversationsActivity.class);
1552        viewConversationIntent.setAction(ConversationsActivity.ACTION_VIEW_CONVERSATION);
1553        viewConversationIntent.putExtra(ConversationsActivity.EXTRA_CONVERSATION, conversationUuid);
1554        if (downloadMessageUuid != null) {
1555            viewConversationIntent.putExtra(
1556                    ConversationsActivity.EXTRA_DOWNLOAD_UUID, downloadMessageUuid);
1557            return PendingIntent.getActivity(
1558                    mXmppConnectionService,
1559                    generateRequestCode(conversationUuid, 8),
1560                    viewConversationIntent,
1561                    s()
1562                            ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1563                            : PendingIntent.FLAG_UPDATE_CURRENT);
1564        } else {
1565            return PendingIntent.getActivity(
1566                    mXmppConnectionService,
1567                    generateRequestCode(conversationUuid, 10),
1568                    viewConversationIntent,
1569                    s()
1570                            ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1571                            : PendingIntent.FLAG_UPDATE_CURRENT);
1572        }
1573    }
1574
1575    private int generateRequestCode(String uuid, int actionId) {
1576        return (actionId * NOTIFICATION_ID_MULTIPLIER)
1577                + (uuid.hashCode() % NOTIFICATION_ID_MULTIPLIER);
1578    }
1579
1580    private int generateRequestCode(Conversational conversation, int actionId) {
1581        return generateRequestCode(conversation.getUuid(), actionId);
1582    }
1583
1584    private PendingIntent createDownloadIntent(final Message message) {
1585        return createContentIntent(message.getConversationUuid(), message.getUuid());
1586    }
1587
1588    private PendingIntent createContentIntent(final Conversational conversation) {
1589        return createContentIntent(conversation.getUuid(), null);
1590    }
1591
1592    private PendingIntent createDeleteIntent(final Conversation conversation) {
1593        final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
1594        intent.setAction(XmppConnectionService.ACTION_CLEAR_MESSAGE_NOTIFICATION);
1595        if (conversation != null) {
1596            intent.putExtra("uuid", conversation.getUuid());
1597            return PendingIntent.getService(
1598                    mXmppConnectionService,
1599                    generateRequestCode(conversation, 20),
1600                    intent,
1601                    s()
1602                            ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1603                            : PendingIntent.FLAG_UPDATE_CURRENT);
1604        }
1605        return PendingIntent.getService(
1606                mXmppConnectionService,
1607                0,
1608                intent,
1609                s()
1610                        ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1611                        : PendingIntent.FLAG_UPDATE_CURRENT);
1612    }
1613
1614    private PendingIntent createMissedCallsDeleteIntent(final Conversational conversation) {
1615        final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
1616        intent.setAction(XmppConnectionService.ACTION_CLEAR_MISSED_CALL_NOTIFICATION);
1617        if (conversation != null) {
1618            intent.putExtra("uuid", conversation.getUuid());
1619            return PendingIntent.getService(
1620                    mXmppConnectionService,
1621                    generateRequestCode(conversation, 21),
1622                    intent,
1623                    s()
1624                            ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1625                            : PendingIntent.FLAG_UPDATE_CURRENT);
1626        }
1627        return PendingIntent.getService(
1628                mXmppConnectionService,
1629                1,
1630                intent,
1631                s()
1632                        ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1633                        : PendingIntent.FLAG_UPDATE_CURRENT);
1634    }
1635
1636    private PendingIntent createReplyIntent(
1637            final Conversation conversation,
1638            final String lastMessageUuid,
1639            final boolean dismissAfterReply) {
1640        final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
1641        intent.setAction(XmppConnectionService.ACTION_REPLY_TO_CONVERSATION);
1642        intent.putExtra("uuid", conversation.getUuid());
1643        intent.putExtra("dismiss_notification", dismissAfterReply);
1644        intent.putExtra("last_message_uuid", lastMessageUuid);
1645        final int id = generateRequestCode(conversation, dismissAfterReply ? 12 : 14);
1646        return PendingIntent.getService(
1647                mXmppConnectionService,
1648                id,
1649                intent,
1650                s()
1651                        ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1652                        : PendingIntent.FLAG_UPDATE_CURRENT);
1653    }
1654
1655    private PendingIntent createReadPendingIntent(Conversation conversation) {
1656        final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
1657        intent.setAction(XmppConnectionService.ACTION_MARK_AS_READ);
1658        intent.putExtra("uuid", conversation.getUuid());
1659        intent.setPackage(mXmppConnectionService.getPackageName());
1660        return PendingIntent.getService(
1661                mXmppConnectionService,
1662                generateRequestCode(conversation, 16),
1663                intent,
1664                s()
1665                        ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1666                        : PendingIntent.FLAG_UPDATE_CURRENT);
1667    }
1668
1669    private PendingIntent createCallAction(String sessionId, final String action, int requestCode) {
1670        return pendingServiceIntent(mXmppConnectionService, action, requestCode, ImmutableMap.of(RtpSessionActivity.EXTRA_SESSION_ID, sessionId));
1671    }
1672
1673    private PendingIntent createSnoozeIntent(final Conversation conversation) {
1674        return pendingServiceIntent(mXmppConnectionService, XmppConnectionService.ACTION_SNOOZE, generateRequestCode(conversation,22),ImmutableMap.of("uuid",conversation.getUuid()));
1675    }
1676
1677    private static PendingIntent pendingServiceIntent(final Context context, final String action, final int requestCode) {
1678        return pendingServiceIntent(context, action, requestCode, ImmutableMap.of());
1679    }
1680
1681    private static PendingIntent pendingServiceIntent(final Context context, final String action, final int requestCode, final Map<String,String> extras) {
1682        final Intent intent = new Intent(context, XmppConnectionService.class);
1683        intent.setAction(action);
1684        for(final Map.Entry<String,String> entry : extras.entrySet()) {
1685            intent.putExtra(entry.getKey(), entry.getValue());
1686        }
1687        return PendingIntent.getService(
1688                context,
1689                requestCode,
1690                intent,
1691                s()
1692                        ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1693                        : PendingIntent.FLAG_UPDATE_CURRENT);
1694    }
1695
1696    private boolean wasHighlightedOrPrivate(final Message message) {
1697        if (message.getConversation() instanceof Conversation) {
1698            Conversation conversation = (Conversation) message.getConversation();
1699            final String nick = conversation.getMucOptions().getActualNick();
1700            final Pattern highlight = generateNickHighlightPattern(nick);
1701            if (message.getBody() == null || nick == null) {
1702                return false;
1703            }
1704            final Matcher m = highlight.matcher(message.getBody());
1705            return (m.find() || message.isPrivateMessage());
1706        } else {
1707            return false;
1708        }
1709    }
1710
1711    public void setOpenConversation(final Conversation conversation) {
1712        this.mOpenConversation = conversation;
1713    }
1714
1715    public void setIsInForeground(final boolean foreground) {
1716        this.mIsInForeground = foreground;
1717    }
1718
1719    private int getPixel(final int dp) {
1720        final DisplayMetrics metrics = mXmppConnectionService.getResources().getDisplayMetrics();
1721        return ((int) (dp * metrics.density));
1722    }
1723
1724    private void markLastNotification() {
1725        this.mLastNotification = SystemClock.elapsedRealtime();
1726    }
1727
1728    private boolean inMiniGracePeriod(final Account account) {
1729        final int miniGrace =
1730                account.getStatus() == Account.State.ONLINE
1731                        ? Config.MINI_GRACE_PERIOD
1732                        : Config.MINI_GRACE_PERIOD * 2;
1733        return SystemClock.elapsedRealtime() < (this.mLastNotification + miniGrace);
1734    }
1735
1736    Notification createForegroundNotification() {
1737        final Notification.Builder mBuilder = new Notification.Builder(mXmppConnectionService);
1738        mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.app_name));
1739        final List<Account> accounts = mXmppConnectionService.getAccounts();
1740        final int enabled;
1741        final int connected;
1742        if (accounts == null) {
1743            enabled = 0;
1744            connected = 0;
1745        } else {
1746            enabled = Iterables.size(Iterables.filter(accounts, Account::isEnabled));
1747            connected =
1748                    Iterables.size(Iterables.filter(accounts, Account::isOnlineAndConnected));
1749        }
1750        mBuilder.setContentText(
1751                mXmppConnectionService.getString(R.string.connected_accounts, connected, enabled));
1752        final PendingIntent openIntent = createOpenConversationsIntent();
1753        if (openIntent != null) {
1754            mBuilder.setContentIntent(openIntent);
1755        }
1756        mBuilder.setWhen(0)
1757                .setPriority(Notification.PRIORITY_MIN)
1758                .setSmallIcon(
1759                        connected > 0
1760                                ? R.drawable.ic_link_white_24dp
1761                                : R.drawable.ic_link_off_white_24dp)
1762                .setLocalOnly(true);
1763
1764        if (Compatibility.runsTwentySix()) {
1765            mBuilder.setChannelId("foreground");
1766            mBuilder.addAction(
1767                    R.drawable.ic_logout_white_24dp,
1768                    mXmppConnectionService.getString(R.string.log_out),
1769                    pendingServiceIntent(
1770                            mXmppConnectionService,
1771                            XmppConnectionService.ACTION_TEMPORARILY_DISABLE,
1772                            87));
1773            mBuilder.addAction(
1774                    R.drawable.ic_notifications_off_white_24dp,
1775                    mXmppConnectionService.getString(R.string.hide_notification),
1776                    pendingNotificationSettingsIntent(mXmppConnectionService));
1777        }
1778
1779        return mBuilder.build();
1780    }
1781
1782    @RequiresApi(api = Build.VERSION_CODES.O)
1783    private static PendingIntent pendingNotificationSettingsIntent(final Context context) {
1784        final Intent intent = new Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS);
1785        intent.putExtra(Settings.EXTRA_APP_PACKAGE, context.getPackageName());
1786        intent.putExtra(Settings.EXTRA_CHANNEL_ID, "foreground");
1787        return PendingIntent.getActivity(
1788                context,
1789                89,
1790                intent,
1791                s()
1792                        ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1793                        : PendingIntent.FLAG_UPDATE_CURRENT);
1794    }
1795
1796    private PendingIntent createOpenConversationsIntent() {
1797        try {
1798            return PendingIntent.getActivity(
1799                    mXmppConnectionService,
1800                    0,
1801                    new Intent(mXmppConnectionService, ConversationsActivity.class),
1802                    s()
1803                            ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1804                            : PendingIntent.FLAG_UPDATE_CURRENT);
1805        } catch (RuntimeException e) {
1806            return null;
1807        }
1808    }
1809
1810    void updateErrorNotification() {
1811        if (Config.SUPPRESS_ERROR_NOTIFICATION) {
1812            cancel(ERROR_NOTIFICATION_ID);
1813            return;
1814        }
1815        final boolean showAllErrors = QuickConversationsService.isConversations();
1816        final List<Account> errors = new ArrayList<>();
1817        boolean torNotAvailable = false;
1818        for (final Account account : mXmppConnectionService.getAccounts()) {
1819            if (account.hasErrorStatus()
1820                    && account.showErrorNotification()
1821                    && (showAllErrors
1822                            || account.getLastErrorStatus() == Account.State.UNAUTHORIZED)) {
1823                errors.add(account);
1824                torNotAvailable |= account.getStatus() == Account.State.TOR_NOT_AVAILABLE;
1825            }
1826        }
1827        if (mXmppConnectionService.foregroundNotificationNeedsUpdatingWhenErrorStateChanges()) {
1828            notify(FOREGROUND_NOTIFICATION_ID, createForegroundNotification());
1829        }
1830        final Notification.Builder mBuilder = new Notification.Builder(mXmppConnectionService);
1831        if (errors.size() == 0) {
1832            cancel(ERROR_NOTIFICATION_ID);
1833            return;
1834        } else if (errors.size() == 1) {
1835            mBuilder.setContentTitle(
1836                    mXmppConnectionService.getString(R.string.problem_connecting_to_account));
1837            mBuilder.setContentText(errors.get(0).getJid().asBareJid().toEscapedString());
1838        } else {
1839            mBuilder.setContentTitle(
1840                    mXmppConnectionService.getString(R.string.problem_connecting_to_accounts));
1841            mBuilder.setContentText(mXmppConnectionService.getString(R.string.touch_to_fix));
1842        }
1843        mBuilder.addAction(
1844                R.drawable.ic_autorenew_white_24dp,
1845                mXmppConnectionService.getString(R.string.try_again),
1846                pendingServiceIntent(mXmppConnectionService, XmppConnectionService.ACTION_TRY_AGAIN, 45));
1847        if (torNotAvailable) {
1848            if (TorServiceUtils.isOrbotInstalled(mXmppConnectionService)) {
1849                mBuilder.addAction(
1850                        R.drawable.ic_play_circle_filled_white_48dp,
1851                        mXmppConnectionService.getString(R.string.start_orbot),
1852                        PendingIntent.getActivity(
1853                                mXmppConnectionService,
1854                                147,
1855                                TorServiceUtils.LAUNCH_INTENT,
1856                                s()
1857                                        ? PendingIntent.FLAG_IMMUTABLE
1858                                                | PendingIntent.FLAG_UPDATE_CURRENT
1859                                        : PendingIntent.FLAG_UPDATE_CURRENT));
1860            } else {
1861                mBuilder.addAction(
1862                        R.drawable.ic_file_download_white_24dp,
1863                        mXmppConnectionService.getString(R.string.install_orbot),
1864                        PendingIntent.getActivity(
1865                                mXmppConnectionService,
1866                                146,
1867                                TorServiceUtils.INSTALL_INTENT,
1868                                s()
1869                                        ? PendingIntent.FLAG_IMMUTABLE
1870                                                | PendingIntent.FLAG_UPDATE_CURRENT
1871                                        : PendingIntent.FLAG_UPDATE_CURRENT));
1872            }
1873        }
1874        mBuilder.setDeleteIntent(pendingServiceIntent(mXmppConnectionService,XmppConnectionService.ACTION_DISMISS_ERROR_NOTIFICATIONS, 69));
1875        mBuilder.setVisibility(Notification.VISIBILITY_PRIVATE);
1876        mBuilder.setSmallIcon(R.drawable.ic_warning_white_24dp);
1877        mBuilder.setLocalOnly(true);
1878        mBuilder.setPriority(Notification.PRIORITY_LOW);
1879        final Intent intent;
1880        if (AccountUtils.MANAGE_ACCOUNT_ACTIVITY != null) {
1881            intent = new Intent(mXmppConnectionService, AccountUtils.MANAGE_ACCOUNT_ACTIVITY);
1882        } else {
1883            intent = new Intent(mXmppConnectionService, EditAccountActivity.class);
1884            intent.putExtra("jid", errors.get(0).getJid().asBareJid().toEscapedString());
1885            intent.putExtra(EditAccountActivity.EXTRA_OPENED_FROM_NOTIFICATION, true);
1886        }
1887        mBuilder.setContentIntent(
1888                PendingIntent.getActivity(
1889                        mXmppConnectionService,
1890                        145,
1891                        intent,
1892                        s()
1893                                ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1894                                : PendingIntent.FLAG_UPDATE_CURRENT));
1895        if (Compatibility.runsTwentySix()) {
1896            mBuilder.setChannelId("error");
1897        }
1898        notify(ERROR_NOTIFICATION_ID, mBuilder.build());
1899    }
1900
1901    void updateFileAddingNotification(int current, Message message) {
1902        Notification.Builder mBuilder = new Notification.Builder(mXmppConnectionService);
1903        mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.transcoding_video));
1904        mBuilder.setProgress(100, current, false);
1905        mBuilder.setSmallIcon(R.drawable.ic_hourglass_empty_white_24dp);
1906        mBuilder.setContentIntent(createContentIntent(message.getConversation()));
1907        mBuilder.setOngoing(true);
1908        if (Compatibility.runsTwentySix()) {
1909            mBuilder.setChannelId("compression");
1910        }
1911        Notification notification = mBuilder.build();
1912        notify(FOREGROUND_NOTIFICATION_ID, notification);
1913    }
1914
1915    private void notify(String tag, int id, Notification notification) {
1916        final NotificationManagerCompat notificationManager =
1917                NotificationManagerCompat.from(mXmppConnectionService);
1918        try {
1919            notificationManager.notify(tag, id, notification);
1920        } catch (RuntimeException e) {
1921            Log.d(Config.LOGTAG, "unable to make notification", e);
1922        }
1923    }
1924
1925    public void notify(int id, Notification notification) {
1926        final NotificationManagerCompat notificationManager =
1927                NotificationManagerCompat.from(mXmppConnectionService);
1928        try {
1929            notificationManager.notify(id, notification);
1930        } catch (RuntimeException e) {
1931            Log.d(Config.LOGTAG, "unable to make notification", e);
1932        }
1933    }
1934
1935    public void cancel(int id) {
1936        final NotificationManagerCompat notificationManager =
1937                NotificationManagerCompat.from(mXmppConnectionService);
1938        try {
1939            notificationManager.cancel(id);
1940        } catch (RuntimeException e) {
1941            Log.d(Config.LOGTAG, "unable to cancel notification", e);
1942        }
1943    }
1944
1945    private void cancel(String tag, int id) {
1946        final NotificationManagerCompat notificationManager =
1947                NotificationManagerCompat.from(mXmppConnectionService);
1948        try {
1949            notificationManager.cancel(tag, id);
1950        } catch (RuntimeException e) {
1951            Log.d(Config.LOGTAG, "unable to cancel notification", e);
1952        }
1953    }
1954
1955    private static class MissedCallsInfo {
1956        private int numberOfCalls;
1957        private long lastTime;
1958
1959        MissedCallsInfo(final long time) {
1960            numberOfCalls = 1;
1961            lastTime = time;
1962        }
1963
1964        public void newMissedCall(final long time) {
1965            ++numberOfCalls;
1966            lastTime = time;
1967        }
1968
1969        public boolean removeMissedCall() {
1970            --numberOfCalls;
1971            return numberOfCalls <= 0;
1972        }
1973
1974        public int getNumberOfCalls() {
1975            return numberOfCalls;
1976        }
1977
1978        public long getLastTime() {
1979            return lastTime;
1980        }
1981    }
1982
1983    private class VibrationRunnable implements Runnable {
1984
1985        @Override
1986        public void run() {
1987            final Vibrator vibrator =
1988                    (Vibrator) mXmppConnectionService.getSystemService(Context.VIBRATOR_SERVICE);
1989            vibrator.vibrate(CALL_PATTERN, -1);
1990        }
1991    }
1992}