NotificationService.java

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