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