initial work toward api 26+

Daniel Gultsch created

* introduce notification channels
* always use foreground service on 26+

Change summary

build.gradle                                                                   |    6 
gradle.properties                                                              |    1 
gradle/wrapper/gradle-wrapper.properties                                       |    2 
src/main/AndroidManifest.xml                                                   |    1 
src/main/java/eu/siacs/conversations/services/BarcodeProvider.java             |    2 
src/main/java/eu/siacs/conversations/services/ContactChooserTargetService.java |    2 
src/main/java/eu/siacs/conversations/services/EventReceiver.java               |   21 
src/main/java/eu/siacs/conversations/services/ExportLogsService.java           |   15 
src/main/java/eu/siacs/conversations/services/NotificationService.java         | 1672 
src/main/java/eu/siacs/conversations/services/XmppConnectionService.java       |   34 
src/main/java/eu/siacs/conversations/ui/SettingsActivity.java                  |    3 
src/main/java/eu/siacs/conversations/ui/SettingsFragment.java                  |    2 
src/main/java/eu/siacs/conversations/ui/XmppActivity.java                      |    8 
src/main/java/eu/siacs/conversations/utils/Compatibility.java                  |   62 
src/main/java/eu/siacs/conversations/utils/ConversationsFileObserver.java      |   11 
src/main/res/values/strings.xml                                                |   11 
src/main/res/xml/preferences.xml                                               |  135 
17 files changed, 1,074 insertions(+), 914 deletions(-)

Detailed changes

build.gradle 🔗

@@ -60,13 +60,13 @@ ext {
 }
 
 android {
-    compileSdkVersion 27
+    compileSdkVersion 28
 
     defaultConfig {
         minSdkVersion 19
-        targetSdkVersion 25
+        targetSdkVersion 28
         versionCode 283
-        versionName "2.2.9"
+        versionName "2.3.0-alpha"
         archivesBaseName += "-$versionName"
         applicationId "eu.siacs.conversations"
         resValue "string", "applicationId", applicationId

gradle.properties 🔗

@@ -13,4 +13,3 @@
 # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
 # org.gradle.parallel=true
 org.gradle.jvmargs=-Xmx2048M
-org.gradle.configureondemand=false

gradle/wrapper/gradle-wrapper.properties 🔗

@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-4.9-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.10-all.zip

src/main/AndroidManifest.xml 🔗

@@ -16,6 +16,7 @@
     <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
     <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
     <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
+    <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
 
     <uses-feature
         android:name="android.hardware.location"

src/main/java/eu/siacs/conversations/services/BarcodeProvider.java 🔗

@@ -14,6 +14,7 @@ import android.os.CancellationSignal;
 import android.os.IBinder;
 import android.os.ParcelFileDescriptor;
 import android.support.annotation.Nullable;
+import android.support.v4.content.ContextCompat;
 import android.util.Log;
 
 import com.google.zxing.BarcodeFormat;
@@ -162,7 +163,6 @@ public class BarcodeProvider extends ContentProvider implements ServiceConnectio
 			synchronized (this) {
 				if (mXmppConnectionService == null && !mBindingInProcess) {
 					Log.d(Config.LOGTAG, "calling to bind service");
-					context.startService(intent);
 					context.bindService(intent, this, Context.BIND_AUTO_CREATE);
 					this.mBindingInProcess = true;
 				}

src/main/java/eu/siacs/conversations/services/ContactChooserTargetService.java 🔗

@@ -12,6 +12,7 @@ import android.os.Bundle;
 import android.os.IBinder;
 import android.service.chooser.ChooserTarget;
 import android.service.chooser.ChooserTargetService;
+import android.support.v4.content.ContextCompat;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -32,7 +33,6 @@ public class ContactChooserTargetService extends ChooserTargetService implements
 	public List<ChooserTarget> onGetChooserTargets(ComponentName targetActivityName, IntentFilter matchedFilter) {
 		Intent intent = new Intent(this, XmppConnectionService.class);
 		intent.setAction("contact_chooser");
-		startService(intent);
 		bindService(intent, this, Context.BIND_AUTO_CREATE);
 		ArrayList<ChooserTarget> chooserTargets = new ArrayList<>();
 		try {

src/main/java/eu/siacs/conversations/services/EventReceiver.java 🔗

@@ -3,38 +3,37 @@ package eu.siacs.conversations.services;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
-import android.content.SharedPreferences;
 import android.preference.PreferenceManager;
+import android.support.v4.content.ContextCompat;
 import android.util.Log;
 
 import eu.siacs.conversations.Config;
-import eu.siacs.conversations.persistance.DatabaseBackend;
 
 public class EventReceiver extends BroadcastReceiver {
 
 	public static final String SETTING_ENABLED_ACCOUNTS = "enabled_accounts";
 
 	@Override
-	public void onReceive(Context context, Intent intent) {
-		Intent mIntentForService = new Intent(context, XmppConnectionService.class);
-		if (intent.getAction() != null) {
-			mIntentForService.setAction(intent.getAction());
+	public void onReceive(final Context context, final Intent originalIntent) {
+		final Intent intentForService = new Intent(context, XmppConnectionService.class);
+		if (originalIntent.getAction() != null) {
+			intentForService.setAction(originalIntent.getAction());
 		} else {
-			mIntentForService.setAction("other");
+			intentForService.setAction("other");
 		}
-		final String action = intent.getAction();
+		final String action = originalIntent.getAction();
 		if (action.equals("ui") || hasEnabledAccounts(context)) {
 			try {
-				context.startService(mIntentForService);
+				ContextCompat.startForegroundService(context, intentForService);
 			} catch (RuntimeException e) {
 				Log.d(Config.LOGTAG,"EventReceiver was unable to start service");
 			}
 		} else {
-			Log.d(Config.LOGTAG,"EventReceiver ignored action "+mIntentForService.getAction());
+			Log.d(Config.LOGTAG,"EventReceiver ignored action "+intentForService.getAction());
 		}
 	}
 
-	public static boolean hasEnabledAccounts(Context context) {
+	public static boolean hasEnabledAccounts(final Context context) {
 		return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTING_ENABLED_ACCOUNTS,true);
 	}
 

src/main/java/eu/siacs/conversations/services/ExportLogsService.java 🔗

@@ -42,15 +42,12 @@ public class ExportLogsService extends Service {
 	@Override
 	public int onStartCommand(Intent intent, int flags, int startId) {
 		if (running.compareAndSet(false, true)) {
-			new Thread(new Runnable() {
-				@Override
-				public void run() {
-					export();
-					stopForeground(true);
-					running.set(false);
-					stopSelf();
-				}
-			}).start();
+			new Thread(() -> {
+                export();
+                stopForeground(true);
+                running.set(false);
+                stopSelf();
+            }).start();
 		}
 		return START_NOT_STICKY;
 	}

src/main/java/eu/siacs/conversations/services/NotificationService.java 🔗

@@ -1,16 +1,23 @@
 package eu.siacs.conversations.services;
 
 import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationChannelGroup;
+import android.app.NotificationManager;
 import android.app.PendingIntent;
+import android.content.Context;
 import android.content.Intent;
 import android.content.SharedPreferences;
 import android.content.res.Resources;
 import android.graphics.Bitmap;
 import android.graphics.Typeface;
+import android.media.AudioAttributes;
+import android.media.RingtoneManager;
 import android.net.Uri;
 import android.os.Build;
 import android.os.SystemClock;
 import android.preference.PreferenceManager;
+import android.support.annotation.RequiresApi;
 import android.support.v4.app.NotificationCompat;
 import android.support.v4.app.NotificationCompat.BigPictureStyle;
 import android.support.v4.app.NotificationCompat.Builder;
@@ -25,7 +32,6 @@ import android.util.Log;
 import android.util.Pair;
 
 import java.io.File;
-import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Calendar;
@@ -49,813 +55,871 @@ import eu.siacs.conversations.persistance.FileBackend;
 import eu.siacs.conversations.ui.ConversationsActivity;
 import eu.siacs.conversations.ui.ManageAccountActivity;
 import eu.siacs.conversations.ui.TimePreference;
+import eu.siacs.conversations.utils.Compatibility;
 import eu.siacs.conversations.utils.GeoHelper;
 import eu.siacs.conversations.utils.UIHelper;
 import eu.siacs.conversations.xmpp.XmppConnection;
 
 public class NotificationService {
 
-	public static final Object CATCHUP_LOCK = new Object();
-
-	private static final String CONVERSATIONS_GROUP = "eu.siacs.conversations";
-	private final XmppConnectionService mXmppConnectionService;
-
-	private final LinkedHashMap<String, ArrayList<Message>> notifications = new LinkedHashMap<>();
-
-	private static final int NOTIFICATION_ID_MULTIPLIER = 1024 * 1024;
-
-	public static final int NOTIFICATION_ID = 2 * NOTIFICATION_ID_MULTIPLIER;
-	public static final int FOREGROUND_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 4;
-	public static final int ERROR_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 6;
-
-	private Conversation mOpenConversation;
-	private boolean mIsInForeground;
-	private long mLastNotification;
-
-	private final HashMap<Conversation, AtomicInteger> mBacklogMessageCounter = new HashMap<>();
-
-	public NotificationService(final XmppConnectionService service) {
-		this.mXmppConnectionService = service;
-	}
-
-	public boolean notify(final Message message) {
-		final Conversation conversation = (Conversation) message.getConversation();
-		return message.getStatus() == Message.STATUS_RECEIVED
-				&& notificationsEnabled()
-				&& !conversation.isMuted()
-				&& (conversation.alwaysNotify() || wasHighlightedOrPrivate(message))
-				&& (!conversation.isWithStranger() || notificationsFromStrangers())
-				;
-	}
-
-	public boolean notificationsEnabled() {
-		return mXmppConnectionService.getBooleanPreference("show_notification", R.bool.show_notification);
-	}
-
-	private boolean notificationsFromStrangers() {
-		return mXmppConnectionService.getBooleanPreference("notifications_from_strangers", R.bool.notifications_from_strangers);
-	}
-
-	public boolean isQuietHours() {
-		if (!mXmppConnectionService.getBooleanPreference("enable_quiet_hours", R.bool.enable_quiet_hours)) {
-			return false;
-		}
-		final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mXmppConnectionService);
-		final long startTime = preferences.getLong("quiet_hours_start", TimePreference.DEFAULT_VALUE) % Config.MILLISECONDS_IN_DAY;
-		final long endTime = preferences.getLong("quiet_hours_end", TimePreference.DEFAULT_VALUE) % Config.MILLISECONDS_IN_DAY;
-		final long nowTime = Calendar.getInstance().getTimeInMillis() % Config.MILLISECONDS_IN_DAY;
-
-		if (endTime < startTime) {
-			return nowTime > startTime || nowTime < endTime;
-		} else {
-			return nowTime > startTime && nowTime < endTime;
-		}
-	}
-
-	public void pushFromBacklog(final Message message) {
-		if (notify(message)) {
-			synchronized (notifications) {
-				getBacklogMessageCounter((Conversation) message.getConversation()).incrementAndGet();
-				pushToStack(message);
-			}
-		}
-	}
-
-	private AtomicInteger getBacklogMessageCounter(Conversation conversation) {
-		synchronized (mBacklogMessageCounter) {
-			if (!mBacklogMessageCounter.containsKey(conversation)) {
-				mBacklogMessageCounter.put(conversation, new AtomicInteger(0));
-			}
-			return mBacklogMessageCounter.get(conversation);
-		}
-	}
-
-	public void pushFromDirectReply(final Message message) {
-		synchronized (notifications) {
-			pushToStack(message);
-			updateNotification(false);
-		}
-	}
-
-	public void finishBacklog(boolean notify, Account account) {
-		synchronized (notifications) {
-			mXmppConnectionService.updateUnreadCountBadge();
-			if (account == null || !notify) {
-				updateNotification(notify);
-			} else {
-				updateNotification(getBacklogMessageCount(account) > 0);
-			}
-		}
-	}
-
-	private int getBacklogMessageCount(Account account) {
-		int count = 0;
-		synchronized (this.mBacklogMessageCounter) {
-			for (Iterator<Map.Entry<Conversation, AtomicInteger>> it = mBacklogMessageCounter.entrySet().iterator(); it.hasNext(); ) {
-				Map.Entry<Conversation, AtomicInteger> entry = it.next();
-				if (entry.getKey().getAccount() == account) {
-					count += entry.getValue().get();
-					it.remove();
-				}
-			}
-		}
-		Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": backlog message count=" + count);
-		return count;
-	}
-
-	public void finishBacklog(boolean notify) {
-		finishBacklog(notify, null);
-	}
-
-	private void pushToStack(final Message message) {
-		final String conversationUuid = message.getConversationUuid();
-		if (notifications.containsKey(conversationUuid)) {
-			notifications.get(conversationUuid).add(message);
-		} else {
-			final ArrayList<Message> mList = new ArrayList<>();
-			mList.add(message);
-			notifications.put(conversationUuid, mList);
-		}
-	}
-
-	public void push(final Message message) {
-		synchronized (CATCHUP_LOCK) {
-			final XmppConnection connection = message.getConversation().getAccount().getXmppConnection();
-			if (connection != null && connection.isWaitingForSmCatchup()) {
-				connection.incrementSmCatchupMessageCounter();
-				pushFromBacklog(message);
-			} else {
-				pushNow(message);
-			}
-		}
-	}
-
-	private void pushNow(final Message message) {
-		mXmppConnectionService.updateUnreadCountBadge();
-		if (!notify(message)) {
-			Log.d(Config.LOGTAG, message.getConversation().getAccount().getJid().asBareJid() + ": suppressing notification because turned off");
-			return;
-		}
-		final boolean isScreenOn = mXmppConnectionService.isInteractive();
-		if (this.mIsInForeground && isScreenOn && this.mOpenConversation == message.getConversation()) {
-			Log.d(Config.LOGTAG, message.getConversation().getAccount().getJid().asBareJid() + ": suppressing notification because conversation is open");
-			return;
-		}
-		synchronized (notifications) {
-			pushToStack(message);
-			final Account account = message.getConversation().getAccount();
-			final boolean doNotify = (!(this.mIsInForeground && this.mOpenConversation == null) || !isScreenOn)
-					&& !account.inGracePeriod()
-					&& !this.inMiniGracePeriod(account);
-			updateNotification(doNotify);
-		}
-	}
-
-	public void clear() {
-		synchronized (notifications) {
-			for (ArrayList<Message> messages : notifications.values()) {
-				markAsReadIfHasDirectReply(messages);
-			}
-			notifications.clear();
-			updateNotification(false);
-		}
-	}
-
-	public void clear(final Conversation conversation) {
-		synchronized (this.mBacklogMessageCounter) {
-			this.mBacklogMessageCounter.remove(conversation);
-		}
-		synchronized (notifications) {
-			markAsReadIfHasDirectReply(conversation);
-			if (notifications.remove(conversation.getUuid()) != null) {
-				cancel(conversation.getUuid(), NOTIFICATION_ID);
-				updateNotification(false, true);
-			}
-		}
-	}
-
-	private void markAsReadIfHasDirectReply(final Conversation conversation) {
-		markAsReadIfHasDirectReply(notifications.get(conversation.getUuid()));
-	}
-
-	private void markAsReadIfHasDirectReply(final ArrayList<Message> messages) {
-		if (messages != null && messages.size() > 0) {
-			Message last = messages.get(messages.size() - 1);
-			if (last.getStatus() != Message.STATUS_RECEIVED) {
-				if (mXmppConnectionService.markRead((Conversation) last.getConversation(), false)) {
-					mXmppConnectionService.updateConversationUi();
-				}
-			}
-		}
-	}
-
-	private void setNotificationColor(final Builder mBuilder) {
-		mBuilder.setColor(ContextCompat.getColor(mXmppConnectionService, R.color.green600));
-	}
-
-	public void updateNotification(final boolean notify) {
-		updateNotification(notify, false);
-	}
-
-	public void updateNotification(final boolean notify, boolean summaryOnly) {
-		final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mXmppConnectionService);
-
-		if (notifications.size() == 0) {
-			cancel(NOTIFICATION_ID);
-		} else {
-			if (notify) {
-				this.markLastNotification();
-			}
-			final Builder mBuilder;
-			if (notifications.size() == 1 && Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
-				mBuilder = buildSingleConversations(notifications.values().iterator().next());
-				modifyForSoundVibrationAndLight(mBuilder, notify, preferences);
-				notify(NOTIFICATION_ID, mBuilder.build());
-			} else {
-				mBuilder = buildMultipleConversation();
-				modifyForSoundVibrationAndLight(mBuilder, notify, preferences);
-				if (!summaryOnly) {
-					for (Map.Entry<String, ArrayList<Message>> entry : notifications.entrySet()) {
-						Builder singleBuilder = buildSingleConversations(entry.getValue());
-						singleBuilder.setGroup(CONVERSATIONS_GROUP);
-						setNotificationColor(singleBuilder);
-						notify(entry.getKey(), NOTIFICATION_ID, singleBuilder.build());
-					}
-				}
-				notify(NOTIFICATION_ID, mBuilder.build());
-			}
-		}
-	}
-
-
-	private void modifyForSoundVibrationAndLight(Builder mBuilder, boolean notify, SharedPreferences preferences) {
-		final Resources resources = mXmppConnectionService.getResources();
-		final String ringtone = preferences.getString("notification_ringtone", resources.getString(R.string.notification_ringtone));
-		final boolean vibrate = preferences.getBoolean("vibrate_on_notification", resources.getBoolean(R.bool.vibrate_on_notification));
-		final boolean led = preferences.getBoolean("led", resources.getBoolean(R.bool.led));
-		final boolean headsup = preferences.getBoolean("notification_headsup", resources.getBoolean(R.bool.headsup_notifications));
-		if (notify && !isQuietHours()) {
-			if (vibrate) {
-				final int dat = 70;
-				final long[] pattern = {0, 3 * dat, dat, dat};
-				mBuilder.setVibrate(pattern);
-			} else {
-				mBuilder.setVibrate(new long[]{0});
-			}
-			Uri uri = Uri.parse(ringtone);
-			try {
-				mBuilder.setSound(fixRingtoneUri(uri));
-			} catch (SecurityException e) {
-				Log.d(Config.LOGTAG, "unable to use custom notification sound " + uri.toString());
-			}
-		}
-		if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
-			mBuilder.setCategory(Notification.CATEGORY_MESSAGE);
-		}
-		mBuilder.setPriority(notify ? (headsup ? NotificationCompat.PRIORITY_HIGH : NotificationCompat.PRIORITY_DEFAULT) : NotificationCompat.PRIORITY_LOW);
-		setNotificationColor(mBuilder);
-		mBuilder.setDefaults(0);
-		if (led) {
-			mBuilder.setLights(0xff00FF00, 2000, 3000);
-		}
-	}
-
-	private Uri fixRingtoneUri(Uri uri) {
-		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && "file".equals(uri.getScheme())) {
-			return FileBackend.getUriForFile(mXmppConnectionService, new File(uri.getPath()));
-		} else {
-			return uri;
-		}
-	}
-
-	private Builder buildMultipleConversation() {
-		final Builder mBuilder = new NotificationCompat.Builder(
-				mXmppConnectionService);
-		final NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle();
-		style.setBigContentTitle(notifications.size()
-				+ " "
-				+ mXmppConnectionService
-				.getString(R.string.unread_conversations));
-		final StringBuilder names = new StringBuilder();
-		Conversation conversation = null;
-		for (final ArrayList<Message> messages : notifications.values()) {
-			if (messages.size() > 0) {
-				conversation = (Conversation) messages.get(0).getConversation();
-				final String name = conversation.getName().toString();
-				SpannableString styledString;
-				if (Config.HIDE_MESSAGE_TEXT_IN_NOTIFICATION) {
-					int count = messages.size();
-					styledString = new SpannableString(name + ": " + mXmppConnectionService.getResources().getQuantityString(R.plurals.x_messages, count, count));
-					styledString.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), 0);
-					style.addLine(styledString);
-				} else {
-					styledString = new SpannableString(name + ": " + UIHelper.getMessagePreview(mXmppConnectionService, messages.get(0)).first);
-					styledString.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), 0);
-					style.addLine(styledString);
-				}
-				names.append(name);
-				names.append(", ");
-			}
-		}
-		if (names.length() >= 2) {
-			names.delete(names.length() - 2, names.length());
-		}
-		mBuilder.setContentTitle(notifications.size()
-				+ " "
-				+ mXmppConnectionService
-				.getString(R.string.unread_conversations));
-		mBuilder.setContentText(names.toString());
-		mBuilder.setStyle(style);
-		if (conversation != null) {
-			mBuilder.setContentIntent(createContentIntent(conversation));
-		}
-		mBuilder.setGroupSummary(true);
-		mBuilder.setGroup(CONVERSATIONS_GROUP);
-		mBuilder.setDeleteIntent(createDeleteIntent(null));
-		mBuilder.setSmallIcon(R.drawable.ic_notification);
-		return mBuilder;
-	}
-
-	private Builder buildSingleConversations(final ArrayList<Message> messages) {
-		final Builder mBuilder = new NotificationCompat.Builder(mXmppConnectionService);
-		if (messages.size() >= 1) {
-			final Conversation conversation = (Conversation) messages.get(0).getConversation();
-			final UnreadConversation.Builder mUnreadBuilder = new UnreadConversation.Builder(conversation.getName().toString());
-			mBuilder.setLargeIcon(mXmppConnectionService.getAvatarService()
-					.get(conversation, getPixel(64)));
-			mBuilder.setContentTitle(conversation.getName());
-			if (Config.HIDE_MESSAGE_TEXT_IN_NOTIFICATION) {
-				int count = messages.size();
-				mBuilder.setContentText(mXmppConnectionService.getResources().getQuantityString(R.plurals.x_messages, count, count));
-			} else {
-				Message message;
-				if ((message = getImage(messages)) != null) {
-					modifyForImage(mBuilder, mUnreadBuilder, message, messages);
-				} else {
-					modifyForTextOnly(mBuilder, mUnreadBuilder, messages);
-				}
-				RemoteInput remoteInput = new RemoteInput.Builder("text_reply").setLabel(UIHelper.getMessageHint(mXmppConnectionService, conversation)).build();
-				PendingIntent markAsReadPendingIntent = createReadPendingIntent(conversation);
-				NotificationCompat.Action markReadAction = new NotificationCompat.Action.Builder(
-						R.drawable.ic_drafts_white_24dp,
-						mXmppConnectionService.getString(R.string.mark_as_read),
-						markAsReadPendingIntent).build();
-				String replyLabel = mXmppConnectionService.getString(R.string.reply);
-				NotificationCompat.Action replyAction = new NotificationCompat.Action.Builder(
-						R.drawable.ic_send_text_offline,
-						replyLabel,
-						createReplyIntent(conversation, false)).addRemoteInput(remoteInput).build();
-				NotificationCompat.Action wearReplyAction = new NotificationCompat.Action.Builder(R.drawable.ic_wear_reply,
-						replyLabel,
-						createReplyIntent(conversation, true)).addRemoteInput(remoteInput).build();
-				mBuilder.extend(new NotificationCompat.WearableExtender().addAction(wearReplyAction));
-				mUnreadBuilder.setReplyAction(createReplyIntent(conversation, true), remoteInput);
-				mUnreadBuilder.setReadPendingIntent(markAsReadPendingIntent);
-				mBuilder.extend(new NotificationCompat.CarExtender().setUnreadConversation(mUnreadBuilder.build()));
-				int addedActionsCount = 1;
-				mBuilder.addAction(markReadAction);
-				if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
-					mBuilder.addAction(replyAction);
-					++addedActionsCount;
-				}
-
-				if (displaySnoozeAction(messages)) {
-					String label = mXmppConnectionService.getString(R.string.snooze);
-					PendingIntent pendingSnoozeIntent = createSnoozeIntent(conversation);
-					NotificationCompat.Action snoozeAction = new NotificationCompat.Action.Builder(
-							R.drawable.ic_notifications_paused_white_24dp,
-							label,
-							pendingSnoozeIntent).build();
-					mBuilder.addAction(snoozeAction);
-					++addedActionsCount;
-				}
-				if (addedActionsCount < 3) {
-					final Message firstLocationMessage = getFirstLocationMessage(messages);
-					if (firstLocationMessage != null) {
-						String label = mXmppConnectionService.getResources().getString(R.string.show_location);
-						PendingIntent pendingShowLocationIntent = createShowLocationIntent(firstLocationMessage);
-						NotificationCompat.Action locationAction = new NotificationCompat.Action.Builder(
-								R.drawable.ic_room_white_24dp,
-								label,
-								pendingShowLocationIntent).build();
-						mBuilder.addAction(locationAction);
-						++addedActionsCount;
-					}
-				}
-				if (addedActionsCount < 3) {
-					Message firstDownloadableMessage = getFirstDownloadableMessage(messages);
-					if (firstDownloadableMessage != null) {
-						String label = mXmppConnectionService.getResources().getString(R.string.download_x_file, UIHelper.getFileDescriptionString(mXmppConnectionService, firstDownloadableMessage));
-						PendingIntent pendingDownloadIntent = createDownloadIntent(firstDownloadableMessage);
-						NotificationCompat.Action downloadAction = new NotificationCompat.Action.Builder(
-								R.drawable.ic_file_download_white_24dp,
-								label,
-								pendingDownloadIntent).build();
-						mBuilder.addAction(downloadAction);
-						++addedActionsCount;
-					}
-				}
-			}
-			if (conversation.getMode() == Conversation.MODE_SINGLE) {
-				Contact contact = conversation.getContact();
-				Uri systemAccount = contact.getSystemAccount();
-				if (systemAccount != null) {
-					mBuilder.addPerson(systemAccount.toString());
-				}
-			}
-			mBuilder.setWhen(conversation.getLatestMessage().getTimeSent());
-			mBuilder.setSmallIcon(R.drawable.ic_notification);
-			mBuilder.setDeleteIntent(createDeleteIntent(conversation));
-			mBuilder.setContentIntent(createContentIntent(conversation));
-		}
-		return mBuilder;
-	}
-
-	private static boolean displaySnoozeAction(List<Message> messages) {
-		int numberOfMessagesWithoutReply = 0;
-		for (Message message : messages) {
-			if (message.getStatus() == Message.STATUS_RECEIVED) {
-				++numberOfMessagesWithoutReply;
-			} else {
-				return false;
-			}
-		}
-		return numberOfMessagesWithoutReply >= 3;
-	}
-
-	private void modifyForImage(final Builder builder, final UnreadConversation.Builder uBuilder,
-	                            final Message message, final ArrayList<Message> messages) {
-		try {
-			final Bitmap bitmap = mXmppConnectionService.getFileBackend()
-					.getThumbnail(message, getPixel(288), false);
-			final ArrayList<Message> tmp = new ArrayList<>();
-			for (final Message msg : messages) {
-				if (msg.getType() == Message.TYPE_TEXT
-						&& msg.getTransferable() == null) {
-					tmp.add(msg);
-				}
-			}
-			final BigPictureStyle bigPictureStyle = new NotificationCompat.BigPictureStyle();
-			bigPictureStyle.bigPicture(bitmap);
-			if (tmp.size() > 0) {
-				CharSequence text = getMergedBodies(tmp);
-				bigPictureStyle.setSummaryText(text);
-				builder.setContentText(text);
-			} else {
-				builder.setContentText(UIHelper.getFileDescriptionString(mXmppConnectionService, message));
-			}
-			builder.setStyle(bigPictureStyle);
-		} catch (final IOException e) {
-			modifyForTextOnly(builder, uBuilder, messages);
-		}
-	}
-
-	private void modifyForTextOnly(final Builder builder, final UnreadConversation.Builder uBuilder, final ArrayList<Message> messages) {
-		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
-			NotificationCompat.MessagingStyle messagingStyle = new NotificationCompat.MessagingStyle(mXmppConnectionService.getString(R.string.me));
-			final Conversation conversation = (Conversation) messages.get(0).getConversation();
-			if (conversation.getMode() == Conversation.MODE_MULTI) {
-				messagingStyle.setConversationTitle(conversation.getName());
-			}
-			for (Message message : messages) {
-				String sender = message.getStatus() == Message.STATUS_RECEIVED ? UIHelper.getMessageDisplayName(message) : null;
-				messagingStyle.addMessage(UIHelper.getMessagePreview(mXmppConnectionService, message).first, message.getTimeSent(), sender);
-			}
-			builder.setStyle(messagingStyle);
-		} else {
-			if (messages.get(0).getConversation().getMode() == Conversation.MODE_SINGLE) {
-				builder.setStyle(new NotificationCompat.BigTextStyle().bigText(getMergedBodies(messages)));
-				builder.setContentText(UIHelper.getMessagePreview(mXmppConnectionService, messages.get(0)).first);
-			} else {
-				final NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle();
-				SpannableString styledString;
-				for (Message message : messages) {
-					final String name = UIHelper.getMessageDisplayName(message);
-					styledString = new SpannableString(name + ": " + message.getBody());
-					styledString.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), 0);
-					style.addLine(styledString);
-				}
-				builder.setStyle(style);
-				int count = messages.size();
-				if (count == 1) {
-					final String name = UIHelper.getMessageDisplayName(messages.get(0));
-					styledString = new SpannableString(name + ": " + messages.get(0).getBody());
-					styledString.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), 0);
-					builder.setContentText(styledString);
-				} else {
-					builder.setContentText(mXmppConnectionService.getResources().getQuantityString(R.plurals.x_messages, count, count));
-				}
-			}
-		}
-		/** message preview for Android Auto **/
-		for (Message message : messages) {
-			Pair<CharSequence, Boolean> preview = UIHelper.getMessagePreview(mXmppConnectionService, message);
-			// only show user written text
-			if (!preview.second) {
-				uBuilder.addMessage(preview.first.toString());
-				uBuilder.setLatestTimestamp(message.getTimeSent());
-			}
-		}
-	}
-
-	private Message getImage(final Iterable<Message> messages) {
-		Message image = null;
-		for (final Message message : messages) {
-			if (message.getStatus() != Message.STATUS_RECEIVED) {
-				return null;
-			}
-			if (message.getType() != Message.TYPE_TEXT
-					&& message.getTransferable() == null
-					&& message.getEncryption() != Message.ENCRYPTION_PGP
-					&& message.getFileParams().height > 0) {
-				image = message;
-			}
-		}
-		return image;
-	}
-
-	private Message getFirstDownloadableMessage(final Iterable<Message> messages) {
-		for (final Message message : messages) {
-			if (message.getTransferable() != null || (message.getType() == Message.TYPE_TEXT && message.treatAsDownloadable())) {
-				return message;
-			}
-		}
-		return null;
-	}
-
-	private Message getFirstLocationMessage(final Iterable<Message> messages) {
-		for (final Message message : messages) {
-			if (message.isGeoUri()) {
-				return message;
-			}
-		}
-		return null;
-	}
-
-	private CharSequence getMergedBodies(final ArrayList<Message> messages) {
-		final StringBuilder text = new StringBuilder();
-		for (Message message : messages) {
-			if (text.length() != 0) {
-				text.append("\n");
-			}
-			text.append(UIHelper.getMessagePreview(mXmppConnectionService, message).first);
-		}
-		return text.toString();
-	}
-
-	private PendingIntent createShowLocationIntent(final Message message) {
-		Iterable<Intent> intents = GeoHelper.createGeoIntentsFromMessage(mXmppConnectionService, message);
-		for (Intent intent : intents) {
-			if (intent.resolveActivity(mXmppConnectionService.getPackageManager()) != null) {
-				return PendingIntent.getActivity(mXmppConnectionService, generateRequestCode(message.getConversation(), 18), intent, PendingIntent.FLAG_UPDATE_CURRENT);
-			}
-		}
-		return createOpenConversationsIntent();
-	}
-
-	private PendingIntent createContentIntent(final String conversationUuid, final String downloadMessageUuid) {
-		final Intent viewConversationIntent = new Intent(mXmppConnectionService, ConversationsActivity.class);
-		viewConversationIntent.setAction(ConversationsActivity.ACTION_VIEW_CONVERSATION);
-		viewConversationIntent.putExtra(ConversationsActivity.EXTRA_CONVERSATION, conversationUuid);
-		if (downloadMessageUuid != null) {
-			viewConversationIntent.putExtra(ConversationsActivity.EXTRA_DOWNLOAD_UUID, downloadMessageUuid);
-			return PendingIntent.getActivity(mXmppConnectionService,
-					generateRequestCode(conversationUuid, 8),
-					viewConversationIntent,
-					PendingIntent.FLAG_UPDATE_CURRENT);
-		} else {
-			return PendingIntent.getActivity(mXmppConnectionService,
-					generateRequestCode(conversationUuid, 10),
-					viewConversationIntent,
-					PendingIntent.FLAG_UPDATE_CURRENT);
-		}
-	}
-
-	private int generateRequestCode(String uuid, int actionId) {
-		return (actionId * NOTIFICATION_ID_MULTIPLIER) + (uuid.hashCode() % NOTIFICATION_ID_MULTIPLIER);
-	}
-
-	private int generateRequestCode(Conversational conversation, int actionId) {
-		return generateRequestCode(conversation.getUuid(), actionId);
-	}
-
-	private PendingIntent createDownloadIntent(final Message message) {
-		return createContentIntent(message.getConversationUuid(), message.getUuid());
-	}
-
-	private PendingIntent createContentIntent(final Conversational conversation) {
-		return createContentIntent(conversation.getUuid(), null);
-	}
-
-	private PendingIntent createDeleteIntent(Conversation conversation) {
-		final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
-		intent.setAction(XmppConnectionService.ACTION_CLEAR_NOTIFICATION);
-		if (conversation != null) {
-			intent.putExtra("uuid", conversation.getUuid());
-			return PendingIntent.getService(mXmppConnectionService, generateRequestCode(conversation, 20), intent, 0);
-		}
-		return PendingIntent.getService(mXmppConnectionService, 0, intent, 0);
-	}
-
-	private PendingIntent createReplyIntent(Conversation conversation, boolean dismissAfterReply) {
-		final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
-		intent.setAction(XmppConnectionService.ACTION_REPLY_TO_CONVERSATION);
-		intent.putExtra("uuid", conversation.getUuid());
-		intent.putExtra("dismiss_notification", dismissAfterReply);
-		final int id = generateRequestCode(conversation, dismissAfterReply ? 12 : 14);
-		return PendingIntent.getService(mXmppConnectionService, id, intent, 0);
-	}
-
-	private PendingIntent createReadPendingIntent(Conversation conversation) {
-		final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
-		intent.setAction(XmppConnectionService.ACTION_MARK_AS_READ);
-		intent.putExtra("uuid", conversation.getUuid());
-		intent.setPackage(mXmppConnectionService.getPackageName());
-		return PendingIntent.getService(mXmppConnectionService, generateRequestCode(conversation, 16), intent, PendingIntent.FLAG_UPDATE_CURRENT);
-	}
-
-	public PendingIntent createSnoozeIntent(Conversation conversation) {
-		final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
-		intent.setAction(XmppConnectionService.ACTION_SNOOZE);
-		intent.putExtra("uuid", conversation.getUuid());
-		intent.setPackage(mXmppConnectionService.getPackageName());
-		return PendingIntent.getService(mXmppConnectionService, generateRequestCode(conversation, 22), intent, PendingIntent.FLAG_UPDATE_CURRENT);
-	}
-
-	private PendingIntent createTryAgainIntent() {
-		final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
-		intent.setAction(XmppConnectionService.ACTION_TRY_AGAIN);
-		return PendingIntent.getService(mXmppConnectionService, 45, intent, 0);
-	}
-
-	private PendingIntent createDismissErrorIntent() {
-		final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
-		intent.setAction(XmppConnectionService.ACTION_DISMISS_ERROR_NOTIFICATIONS);
-		return PendingIntent.getService(mXmppConnectionService, 69, intent, 0);
-	}
-
-	private boolean wasHighlightedOrPrivate(final Message message) {
-		if (message.getConversation() instanceof Conversation) {
-			Conversation conversation = (Conversation) message.getConversation();
-			final String nick = conversation.getMucOptions().getActualNick();
-			final Pattern highlight = generateNickHighlightPattern(nick);
-			if (message.getBody() == null || nick == null) {
-				return false;
-			}
-			final Matcher m = highlight.matcher(message.getBody());
-			return (m.find() || message.getType() == Message.TYPE_PRIVATE);
-		} else {
-			return false;
-		}
-	}
-
-	public static Pattern generateNickHighlightPattern(final String nick) {
-		return Pattern.compile("(?<=(^|\\s))" + Pattern.quote(nick) + "\\b");
-	}
-
-	public void setOpenConversation(final Conversation conversation) {
-		this.mOpenConversation = conversation;
-	}
-
-	public void setIsInForeground(final boolean foreground) {
-		this.mIsInForeground = foreground;
-	}
-
-	private int getPixel(final int dp) {
-		final DisplayMetrics metrics = mXmppConnectionService.getResources()
-				.getDisplayMetrics();
-		return ((int) (dp * metrics.density));
-	}
-
-	private void markLastNotification() {
-		this.mLastNotification = SystemClock.elapsedRealtime();
-	}
-
-	private boolean inMiniGracePeriod(final Account account) {
-		final int miniGrace = account.getStatus() == Account.State.ONLINE ? Config.MINI_GRACE_PERIOD
-				: Config.MINI_GRACE_PERIOD * 2;
-		return SystemClock.elapsedRealtime() < (this.mLastNotification + miniGrace);
-	}
-
-	public Notification createForegroundNotification() {
-		final NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(mXmppConnectionService);
-
-		mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.conversations_foreground_service));
-		if (Config.SHOW_CONNECTED_ACCOUNTS) {
-			List<Account> accounts = mXmppConnectionService.getAccounts();
-			int enabled = 0;
-			int connected = 0;
-			for (Account account : accounts) {
-				if (account.isOnlineAndConnected()) {
-					connected++;
-					enabled++;
-				} else if (account.isEnabled()) {
-					enabled++;
-				}
-			}
-			mBuilder.setContentText(mXmppConnectionService.getString(R.string.connected_accounts, connected, enabled));
-		} else {
-			mBuilder.setContentText(mXmppConnectionService.getString(R.string.touch_to_open_conversations));
-		}
-		mBuilder.setContentIntent(createOpenConversationsIntent());
-		mBuilder.setWhen(0);
-		mBuilder.setPriority(Config.SHOW_CONNECTED_ACCOUNTS ? NotificationCompat.PRIORITY_DEFAULT : NotificationCompat.PRIORITY_MIN);
-		mBuilder.setSmallIcon(R.drawable.ic_link_white_24dp);
-		return mBuilder.build();
-	}
-
-	private PendingIntent createOpenConversationsIntent() {
-		return PendingIntent.getActivity(mXmppConnectionService, 0, new Intent(mXmppConnectionService, ConversationsActivity.class), 0);
-	}
-
-	public void updateErrorNotification() {
-		if (Config.SUPPRESS_ERROR_NOTIFICATION) {
-			cancel(ERROR_NOTIFICATION_ID);
-			return;
-		}
-		final List<Account> errors = new ArrayList<>();
-		for (final Account account : mXmppConnectionService.getAccounts()) {
-			if (account.hasErrorStatus() && account.showErrorNotification()) {
-				errors.add(account);
-			}
-		}
-		if (mXmppConnectionService.keepForegroundService()) {
-			notify(FOREGROUND_NOTIFICATION_ID, createForegroundNotification());
-		}
-		final NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(mXmppConnectionService);
-		if (errors.size() == 0) {
-			cancel(ERROR_NOTIFICATION_ID);
-			return;
-		} else if (errors.size() == 1) {
-			mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.problem_connecting_to_account));
-			mBuilder.setContentText(errors.get(0).getJid().asBareJid().toString());
-		} else {
-			mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.problem_connecting_to_accounts));
-			mBuilder.setContentText(mXmppConnectionService.getString(R.string.touch_to_fix));
-		}
-		mBuilder.addAction(R.drawable.ic_autorenew_white_24dp,
-				mXmppConnectionService.getString(R.string.try_again),
-				createTryAgainIntent());
-		mBuilder.setDeleteIntent(createDismissErrorIntent());
-		mBuilder.setVisibility(NotificationCompat.VISIBILITY_PRIVATE);
-		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
-			mBuilder.setSmallIcon(R.drawable.ic_warning_white_24dp);
-		} else {
-			mBuilder.setSmallIcon(R.drawable.ic_stat_alert_warning);
-		}
-		mBuilder.setLocalOnly(true);
-		mBuilder.setPriority(NotificationCompat.PRIORITY_LOW);
-		mBuilder.setContentIntent(PendingIntent.getActivity(mXmppConnectionService,
-				145,
-				new Intent(mXmppConnectionService, ManageAccountActivity.class),
-				PendingIntent.FLAG_UPDATE_CURRENT));
-		notify(ERROR_NOTIFICATION_ID, mBuilder.build());
-	}
-
-	public Notification updateFileAddingNotification(int current, Message message) {
-		NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(mXmppConnectionService);
-		mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.transcoding_video));
-		mBuilder.setProgress(100, current, false);
-		mBuilder.setSmallIcon(R.drawable.ic_hourglass_empty_white_24dp);
-		mBuilder.setContentIntent(createContentIntent(message.getConversation()));
-		Notification notification = mBuilder.build();
-		notify(FOREGROUND_NOTIFICATION_ID, notification);
-		return notification;
-	}
-
-	private void notify(String tag, int id, Notification notification) {
-		final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(mXmppConnectionService);
-		try {
-			notificationManager.notify(tag, id, notification);
-		} catch (RuntimeException e) {
-			Log.d(Config.LOGTAG, "unable to make notification", e);
-		}
-	}
-
-	private void notify(int id, Notification notification) {
-		final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(mXmppConnectionService);
-		try {
-			notificationManager.notify(id, notification);
-		} catch (RuntimeException e) {
-			Log.d(Config.LOGTAG, "unable to make notification", e);
-		}
-	}
-
-	private void cancel(int id) {
-		final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(mXmppConnectionService);
-		try {
-			notificationManager.cancel(id);
-		} catch (RuntimeException e) {
-			Log.d(Config.LOGTAG, "unable to cancel notification", e);
-		}
-	}
-
-	private void cancel(String tag, int id) {
-		final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(mXmppConnectionService);
-		try {
-			notificationManager.cancel(tag, id);
-		} catch (RuntimeException e) {
-			Log.d(Config.LOGTAG, "unable to cancel notification", e);
-		}
-	}
+    public static final Object CATCHUP_LOCK = new Object();
+
+    private static final String CONVERSATIONS_GROUP = "eu.siacs.conversations";
+    private static final int NOTIFICATION_ID_MULTIPLIER = 1024 * 1024;
+    private static final int NOTIFICATION_ID = 2 * NOTIFICATION_ID_MULTIPLIER;
+    public static final int FOREGROUND_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 4;
+    private static final int ERROR_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 6;
+    private final XmppConnectionService mXmppConnectionService;
+    private final LinkedHashMap<String, ArrayList<Message>> notifications = new LinkedHashMap<>();
+    private final HashMap<Conversation, AtomicInteger> mBacklogMessageCounter = new HashMap<>();
+    private Conversation mOpenConversation;
+    private boolean mIsInForeground;
+    private long mLastNotification;
+
+    NotificationService(final XmppConnectionService service) {
+        this.mXmppConnectionService = service;
+    }
+
+    private static boolean displaySnoozeAction(List<Message> messages) {
+        int numberOfMessagesWithoutReply = 0;
+        for (Message message : messages) {
+            if (message.getStatus() == Message.STATUS_RECEIVED) {
+                ++numberOfMessagesWithoutReply;
+            } else {
+                return false;
+            }
+        }
+        return numberOfMessagesWithoutReply >= 3;
+    }
+
+    public static Pattern generateNickHighlightPattern(final String nick) {
+        return Pattern.compile("(?<=(^|\\s))" + Pattern.quote(nick) + "\\b");
+    }
+
+    @RequiresApi(api = Build.VERSION_CODES.O)
+    public void initializeChannels() {
+        final Context c = mXmppConnectionService;
+        NotificationManager notificationManager = c.getSystemService(NotificationManager.class);
+        if (notificationManager == null) {
+            return;
+        }
+
+        notificationManager.createNotificationChannelGroup(new NotificationChannelGroup("status", c.getString(R.string.notification_group_status_information)));
+        notificationManager.createNotificationChannelGroup(new NotificationChannelGroup("chats", c.getString(R.string.notification_group_messages)));
+        final NotificationChannel foregroundServiceChannel = new NotificationChannel("foreground",
+                c.getString(R.string.foreground_service_channel_name),
+                NotificationManager.IMPORTANCE_MIN);
+        foregroundServiceChannel.setDescription(c.getString(R.string.foreground_service_channel_description));
+        foregroundServiceChannel.setShowBadge(false);
+        foregroundServiceChannel.setGroup("status");
+        notificationManager.createNotificationChannel(foregroundServiceChannel);
+        final NotificationChannel errorChannel = new NotificationChannel("error",
+                c.getString(R.string.error_channel_name),
+                NotificationManager.IMPORTANCE_LOW);
+        errorChannel.setDescription(c.getString(R.string.error_channel_description));
+        errorChannel.setShowBadge(false);
+        errorChannel.setGroup("status");
+        notificationManager.createNotificationChannel(errorChannel);
+        final NotificationChannel messagesChannel = new NotificationChannel("messages",
+                c.getString(R.string.messages_channel_name),
+                NotificationManager.IMPORTANCE_HIGH);
+        messagesChannel.setShowBadge(true);
+        messagesChannel.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION), new AudioAttributes.Builder()
+                .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
+                .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
+                .build());
+        messagesChannel.setLightColor(0xff00ff00);
+        final int dat = 70;
+        final long[] pattern = {0, 3 * dat, dat, dat};
+        messagesChannel.setVibrationPattern(pattern);
+        messagesChannel.enableVibration(true);
+        messagesChannel.enableLights(true);
+        messagesChannel.setGroup("chats");
+        notificationManager.createNotificationChannel(messagesChannel);
+        final NotificationChannel silentMessagesChannel = new NotificationChannel("silent_messages",
+                c.getString(R.string.silent_messages_channel_name),
+                NotificationManager.IMPORTANCE_LOW);
+        silentMessagesChannel.setDescription(c.getString(R.string.silent_messages_channel_description));
+        silentMessagesChannel.setShowBadge(true);
+        silentMessagesChannel.setLightColor(0xff00ff00);
+        silentMessagesChannel.enableLights(true);
+        silentMessagesChannel.setGroup("chats");
+        notificationManager.createNotificationChannel(silentMessagesChannel);
+    }
+
+    public boolean notify(final Message message) {
+        final Conversation conversation = (Conversation) message.getConversation();
+        return message.getStatus() == Message.STATUS_RECEIVED
+                && notificationsEnabled()
+                && !conversation.isMuted()
+                && (conversation.alwaysNotify() || wasHighlightedOrPrivate(message))
+                && (!conversation.isWithStranger() || notificationsFromStrangers())
+                ;
+    }
+
+    private boolean notificationsEnabled() {
+        return mXmppConnectionService.getBooleanPreference("show_notification", R.bool.show_notification);
+    }
+
+    private boolean notificationsFromStrangers() {
+        return mXmppConnectionService.getBooleanPreference("notifications_from_strangers", R.bool.notifications_from_strangers);
+    }
+
+    private boolean isQuietHours() {
+        if (!mXmppConnectionService.getBooleanPreference("enable_quiet_hours", R.bool.enable_quiet_hours)) {
+            return false;
+        }
+        final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mXmppConnectionService);
+        final long startTime = preferences.getLong("quiet_hours_start", TimePreference.DEFAULT_VALUE) % Config.MILLISECONDS_IN_DAY;
+        final long endTime = preferences.getLong("quiet_hours_end", TimePreference.DEFAULT_VALUE) % Config.MILLISECONDS_IN_DAY;
+        final long nowTime = Calendar.getInstance().getTimeInMillis() % Config.MILLISECONDS_IN_DAY;
+
+        if (endTime < startTime) {
+            return nowTime > startTime || nowTime < endTime;
+        } else {
+            return nowTime > startTime && nowTime < endTime;
+        }
+    }
+
+    public void pushFromBacklog(final Message message) {
+        if (notify(message)) {
+            synchronized (notifications) {
+                getBacklogMessageCounter((Conversation) message.getConversation()).incrementAndGet();
+                pushToStack(message);
+            }
+        }
+    }
+
+    private AtomicInteger getBacklogMessageCounter(Conversation conversation) {
+        synchronized (mBacklogMessageCounter) {
+            if (!mBacklogMessageCounter.containsKey(conversation)) {
+                mBacklogMessageCounter.put(conversation, new AtomicInteger(0));
+            }
+            return mBacklogMessageCounter.get(conversation);
+        }
+    }
+
+    public void pushFromDirectReply(final Message message) {
+        synchronized (notifications) {
+            pushToStack(message);
+            updateNotification(false);
+        }
+    }
+
+    public void finishBacklog(boolean notify, Account account) {
+        synchronized (notifications) {
+            mXmppConnectionService.updateUnreadCountBadge();
+            if (account == null || !notify) {
+                updateNotification(notify);
+            } else {
+                updateNotification(getBacklogMessageCount(account) > 0);
+            }
+        }
+    }
+
+    private int getBacklogMessageCount(Account account) {
+        int count = 0;
+        synchronized (this.mBacklogMessageCounter) {
+            for (Iterator<Map.Entry<Conversation, AtomicInteger>> it = mBacklogMessageCounter.entrySet().iterator(); it.hasNext(); ) {
+                Map.Entry<Conversation, AtomicInteger> entry = it.next();
+                if (entry.getKey().getAccount() == account) {
+                    count += entry.getValue().get();
+                    it.remove();
+                }
+            }
+        }
+        Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": backlog message count=" + count);
+        return count;
+    }
+
+    public void finishBacklog(boolean notify) {
+        finishBacklog(notify, null);
+    }
+
+    private void pushToStack(final Message message) {
+        final String conversationUuid = message.getConversationUuid();
+        if (notifications.containsKey(conversationUuid)) {
+            notifications.get(conversationUuid).add(message);
+        } else {
+            final ArrayList<Message> mList = new ArrayList<>();
+            mList.add(message);
+            notifications.put(conversationUuid, mList);
+        }
+    }
+
+    public void push(final Message message) {
+        synchronized (CATCHUP_LOCK) {
+            final XmppConnection connection = message.getConversation().getAccount().getXmppConnection();
+            if (connection != null && connection.isWaitingForSmCatchup()) {
+                connection.incrementSmCatchupMessageCounter();
+                pushFromBacklog(message);
+            } else {
+                pushNow(message);
+            }
+        }
+    }
+
+    private void pushNow(final Message message) {
+        mXmppConnectionService.updateUnreadCountBadge();
+        if (!notify(message)) {
+            Log.d(Config.LOGTAG, message.getConversation().getAccount().getJid().asBareJid() + ": suppressing notification because turned off");
+            return;
+        }
+        final boolean isScreenOn = mXmppConnectionService.isInteractive();
+        if (this.mIsInForeground && isScreenOn && this.mOpenConversation == message.getConversation()) {
+            Log.d(Config.LOGTAG, message.getConversation().getAccount().getJid().asBareJid() + ": suppressing notification because conversation is open");
+            return;
+        }
+        synchronized (notifications) {
+            pushToStack(message);
+            final Account account = message.getConversation().getAccount();
+            final boolean doNotify = (!(this.mIsInForeground && this.mOpenConversation == null) || !isScreenOn)
+                    && !account.inGracePeriod()
+                    && !this.inMiniGracePeriod(account);
+            updateNotification(doNotify);
+        }
+    }
+
+    public void clear() {
+        synchronized (notifications) {
+            for (ArrayList<Message> messages : notifications.values()) {
+                markAsReadIfHasDirectReply(messages);
+            }
+            notifications.clear();
+            updateNotification(false);
+        }
+    }
+
+    public void clear(final Conversation conversation) {
+        synchronized (this.mBacklogMessageCounter) {
+            this.mBacklogMessageCounter.remove(conversation);
+        }
+        synchronized (notifications) {
+            markAsReadIfHasDirectReply(conversation);
+            if (notifications.remove(conversation.getUuid()) != null) {
+                cancel(conversation.getUuid(), NOTIFICATION_ID);
+                updateNotification(false, true);
+            }
+        }
+    }
+
+    private void markAsReadIfHasDirectReply(final Conversation conversation) {
+        markAsReadIfHasDirectReply(notifications.get(conversation.getUuid()));
+    }
+
+    private void markAsReadIfHasDirectReply(final ArrayList<Message> messages) {
+        if (messages != null && messages.size() > 0) {
+            Message last = messages.get(messages.size() - 1);
+            if (last.getStatus() != Message.STATUS_RECEIVED) {
+                if (mXmppConnectionService.markRead((Conversation) last.getConversation(), false)) {
+                    mXmppConnectionService.updateConversationUi();
+                }
+            }
+        }
+    }
+
+    private void setNotificationColor(final Builder mBuilder) {
+        mBuilder.setColor(ContextCompat.getColor(mXmppConnectionService, R.color.green600));
+    }
+
+    public void updateNotification(final boolean notify) {
+        updateNotification(notify, false);
+    }
+
+    private void updateNotification(final boolean notify, boolean summaryOnly) {
+        final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mXmppConnectionService);
+
+        if (notifications.size() == 0) {
+            cancel(NOTIFICATION_ID);
+        } else {
+            if (notify) {
+                this.markLastNotification();
+            }
+            final Builder mBuilder;
+            if (notifications.size() == 1 && Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
+                mBuilder = buildSingleConversations(notifications.values().iterator().next(), notify);
+                modifyForSoundVibrationAndLight(mBuilder, notify, preferences);
+                notify(NOTIFICATION_ID, mBuilder.build());
+            } else {
+                mBuilder = buildMultipleConversation(notify);
+                mBuilder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN);
+                modifyForSoundVibrationAndLight(mBuilder, notify, preferences);
+                if (!summaryOnly) {
+                    for (Map.Entry<String, ArrayList<Message>> entry : notifications.entrySet()) {
+                        Builder singleBuilder = buildSingleConversations(entry.getValue(), notify);
+                        singleBuilder.setGroup(CONVERSATIONS_GROUP);
+                        setNotificationColor(singleBuilder);
+                        notify(entry.getKey(), NOTIFICATION_ID, singleBuilder.build());
+                    }
+                }
+                notify(NOTIFICATION_ID, mBuilder.build());
+            }
+        }
+    }
+
+    private void modifyForSoundVibrationAndLight(Builder mBuilder, boolean notify, SharedPreferences preferences) {
+        final Resources resources = mXmppConnectionService.getResources();
+        final String ringtone = preferences.getString("notification_ringtone", resources.getString(R.string.notification_ringtone));
+        final boolean vibrate = preferences.getBoolean("vibrate_on_notification", resources.getBoolean(R.bool.vibrate_on_notification));
+        final boolean led = preferences.getBoolean("led", resources.getBoolean(R.bool.led));
+        final boolean headsup = preferences.getBoolean("notification_headsup", resources.getBoolean(R.bool.headsup_notifications));
+        if (notify && !isQuietHours()) {
+            if (vibrate) {
+                final int dat = 70;
+                final long[] pattern = {0, 3 * dat, dat, dat};
+                mBuilder.setVibrate(pattern);
+            } else {
+                mBuilder.setVibrate(new long[]{0});
+            }
+            Uri uri = Uri.parse(ringtone);
+            try {
+                mBuilder.setSound(fixRingtoneUri(uri));
+            } catch (SecurityException e) {
+                Log.d(Config.LOGTAG, "unable to use custom notification sound " + uri.toString());
+            }
+        }
+        if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+            mBuilder.setCategory(Notification.CATEGORY_MESSAGE);
+        }
+        mBuilder.setPriority(notify ? (headsup ? NotificationCompat.PRIORITY_HIGH : NotificationCompat.PRIORITY_DEFAULT) : NotificationCompat.PRIORITY_LOW);
+        setNotificationColor(mBuilder);
+        mBuilder.setDefaults(0);
+        if (led) {
+            mBuilder.setLights(0xff00FF00, 2000, 3000);
+        }
+    }
+
+    private Uri fixRingtoneUri(Uri uri) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && "file".equals(uri.getScheme())) {
+            return FileBackend.getUriForFile(mXmppConnectionService, new File(uri.getPath()));
+        } else {
+            return uri;
+        }
+    }
+
+    private Builder buildMultipleConversation(final boolean notify) {
+        final Builder mBuilder = new NotificationCompat.Builder(mXmppConnectionService, notify ? "messages" : "silent_messages");
+        final NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle();
+        style.setBigContentTitle(notifications.size()
+                + " "
+                + mXmppConnectionService
+                .getString(R.string.unread_conversations));
+        final StringBuilder names = new StringBuilder();
+        Conversation conversation = null;
+        for (final ArrayList<Message> messages : notifications.values()) {
+            if (messages.size() > 0) {
+                conversation = (Conversation) messages.get(0).getConversation();
+                final String name = conversation.getName().toString();
+                SpannableString styledString;
+                if (Config.HIDE_MESSAGE_TEXT_IN_NOTIFICATION) {
+                    int count = messages.size();
+                    styledString = new SpannableString(name + ": " + mXmppConnectionService.getResources().getQuantityString(R.plurals.x_messages, count, count));
+                    styledString.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), 0);
+                    style.addLine(styledString);
+                } else {
+                    styledString = new SpannableString(name + ": " + UIHelper.getMessagePreview(mXmppConnectionService, messages.get(0)).first);
+                    styledString.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), 0);
+                    style.addLine(styledString);
+                }
+                names.append(name);
+                names.append(", ");
+            }
+        }
+        if (names.length() >= 2) {
+            names.delete(names.length() - 2, names.length());
+        }
+        mBuilder.setContentTitle(notifications.size()
+                + " "
+                + mXmppConnectionService
+                .getString(R.string.unread_conversations));
+        mBuilder.setContentText(names.toString());
+        mBuilder.setStyle(style);
+        if (conversation != null) {
+            mBuilder.setContentIntent(createContentIntent(conversation));
+        }
+        mBuilder.setGroupSummary(true);
+        mBuilder.setGroup(CONVERSATIONS_GROUP);
+        mBuilder.setDeleteIntent(createDeleteIntent(null));
+        mBuilder.setSmallIcon(R.drawable.ic_notification);
+        return mBuilder;
+    }
+
+    private Builder buildSingleConversations(final ArrayList<Message> messages, final boolean notify) {
+        final Builder mBuilder = new NotificationCompat.Builder(mXmppConnectionService, notify ? "messages" : "silent_messages");
+        if (messages.size() >= 1) {
+            final Conversation conversation = (Conversation) messages.get(0).getConversation();
+            final UnreadConversation.Builder mUnreadBuilder = new UnreadConversation.Builder(conversation.getName().toString());
+            mBuilder.setLargeIcon(mXmppConnectionService.getAvatarService()
+                    .get(conversation, getPixel(64)));
+            mBuilder.setContentTitle(conversation.getName());
+            if (Config.HIDE_MESSAGE_TEXT_IN_NOTIFICATION) {
+                int count = messages.size();
+                mBuilder.setContentText(mXmppConnectionService.getResources().getQuantityString(R.plurals.x_messages, count, count));
+            } else {
+                Message message;
+                if ((message = getImage(messages)) != null) {
+                    modifyForImage(mBuilder, mUnreadBuilder, message, messages);
+                } else {
+                    modifyForTextOnly(mBuilder, mUnreadBuilder, messages);
+                }
+                RemoteInput remoteInput = new RemoteInput.Builder("text_reply").setLabel(UIHelper.getMessageHint(mXmppConnectionService, conversation)).build();
+                PendingIntent markAsReadPendingIntent = createReadPendingIntent(conversation);
+                NotificationCompat.Action markReadAction = new NotificationCompat.Action.Builder(
+                        R.drawable.ic_drafts_white_24dp,
+                        mXmppConnectionService.getString(R.string.mark_as_read),
+                        markAsReadPendingIntent).build();
+                String replyLabel = mXmppConnectionService.getString(R.string.reply);
+                NotificationCompat.Action replyAction = new NotificationCompat.Action.Builder(
+                        R.drawable.ic_send_text_offline,
+                        replyLabel,
+                        createReplyIntent(conversation, false)).addRemoteInput(remoteInput).build();
+                NotificationCompat.Action wearReplyAction = new NotificationCompat.Action.Builder(R.drawable.ic_wear_reply,
+                        replyLabel,
+                        createReplyIntent(conversation, true)).addRemoteInput(remoteInput).build();
+                mBuilder.extend(new NotificationCompat.WearableExtender().addAction(wearReplyAction));
+                mUnreadBuilder.setReplyAction(createReplyIntent(conversation, true), remoteInput);
+                mUnreadBuilder.setReadPendingIntent(markAsReadPendingIntent);
+                mBuilder.extend(new NotificationCompat.CarExtender().setUnreadConversation(mUnreadBuilder.build()));
+                int addedActionsCount = 1;
+                mBuilder.addAction(markReadAction);
+                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+                    mBuilder.addAction(replyAction);
+                    ++addedActionsCount;
+                }
+
+                if (displaySnoozeAction(messages)) {
+                    String label = mXmppConnectionService.getString(R.string.snooze);
+                    PendingIntent pendingSnoozeIntent = createSnoozeIntent(conversation);
+                    NotificationCompat.Action snoozeAction = new NotificationCompat.Action.Builder(
+                            R.drawable.ic_notifications_paused_white_24dp,
+                            label,
+                            pendingSnoozeIntent).build();
+                    mBuilder.addAction(snoozeAction);
+                    ++addedActionsCount;
+                }
+                if (addedActionsCount < 3) {
+                    final Message firstLocationMessage = getFirstLocationMessage(messages);
+                    if (firstLocationMessage != null) {
+                        String label = mXmppConnectionService.getResources().getString(R.string.show_location);
+                        PendingIntent pendingShowLocationIntent = createShowLocationIntent(firstLocationMessage);
+                        NotificationCompat.Action locationAction = new NotificationCompat.Action.Builder(
+                                R.drawable.ic_room_white_24dp,
+                                label,
+                                pendingShowLocationIntent).build();
+                        mBuilder.addAction(locationAction);
+                        ++addedActionsCount;
+                    }
+                }
+                if (addedActionsCount < 3) {
+                    Message firstDownloadableMessage = getFirstDownloadableMessage(messages);
+                    if (firstDownloadableMessage != null) {
+                        String label = mXmppConnectionService.getResources().getString(R.string.download_x_file, UIHelper.getFileDescriptionString(mXmppConnectionService, firstDownloadableMessage));
+                        PendingIntent pendingDownloadIntent = createDownloadIntent(firstDownloadableMessage);
+                        NotificationCompat.Action downloadAction = new NotificationCompat.Action.Builder(
+                                R.drawable.ic_file_download_white_24dp,
+                                label,
+                                pendingDownloadIntent).build();
+                        mBuilder.addAction(downloadAction);
+                        ++addedActionsCount;
+                    }
+                }
+            }
+            if (conversation.getMode() == Conversation.MODE_SINGLE) {
+                Contact contact = conversation.getContact();
+                Uri systemAccount = contact.getSystemAccount();
+                if (systemAccount != null) {
+                    mBuilder.addPerson(systemAccount.toString());
+                }
+            }
+            mBuilder.setWhen(conversation.getLatestMessage().getTimeSent());
+            mBuilder.setSmallIcon(R.drawable.ic_notification);
+            mBuilder.setDeleteIntent(createDeleteIntent(conversation));
+            mBuilder.setContentIntent(createContentIntent(conversation));
+        }
+        return mBuilder;
+    }
+
+    private void modifyForImage(final Builder builder, final UnreadConversation.Builder uBuilder,
+                                final Message message, final ArrayList<Message> messages) {
+        try {
+            final Bitmap bitmap = mXmppConnectionService.getFileBackend()
+                    .getThumbnail(message, getPixel(288), false);
+            final ArrayList<Message> tmp = new ArrayList<>();
+            for (final Message msg : messages) {
+                if (msg.getType() == Message.TYPE_TEXT
+                        && msg.getTransferable() == null) {
+                    tmp.add(msg);
+                }
+            }
+            final BigPictureStyle bigPictureStyle = new NotificationCompat.BigPictureStyle();
+            bigPictureStyle.bigPicture(bitmap);
+            if (tmp.size() > 0) {
+                CharSequence text = getMergedBodies(tmp);
+                bigPictureStyle.setSummaryText(text);
+                builder.setContentText(text);
+            } else {
+                builder.setContentText(UIHelper.getFileDescriptionString(mXmppConnectionService, message));
+            }
+            builder.setStyle(bigPictureStyle);
+        } catch (final IOException e) {
+            modifyForTextOnly(builder, uBuilder, messages);
+        }
+    }
+
+    private void modifyForTextOnly(final Builder builder, final UnreadConversation.Builder uBuilder, final ArrayList<Message> messages) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+            NotificationCompat.MessagingStyle messagingStyle = new NotificationCompat.MessagingStyle(mXmppConnectionService.getString(R.string.me));
+            final Conversation conversation = (Conversation) messages.get(0).getConversation();
+            if (conversation.getMode() == Conversation.MODE_MULTI) {
+                messagingStyle.setConversationTitle(conversation.getName());
+            }
+            for (Message message : messages) {
+                String sender = message.getStatus() == Message.STATUS_RECEIVED ? UIHelper.getMessageDisplayName(message) : null;
+                messagingStyle.addMessage(UIHelper.getMessagePreview(mXmppConnectionService, message).first, message.getTimeSent(), sender);
+            }
+            builder.setStyle(messagingStyle);
+        } else {
+            if (messages.get(0).getConversation().getMode() == Conversation.MODE_SINGLE) {
+                builder.setStyle(new NotificationCompat.BigTextStyle().bigText(getMergedBodies(messages)));
+                builder.setContentText(UIHelper.getMessagePreview(mXmppConnectionService, messages.get(0)).first);
+            } else {
+                final NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle();
+                SpannableString styledString;
+                for (Message message : messages) {
+                    final String name = UIHelper.getMessageDisplayName(message);
+                    styledString = new SpannableString(name + ": " + message.getBody());
+                    styledString.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), 0);
+                    style.addLine(styledString);
+                }
+                builder.setStyle(style);
+                int count = messages.size();
+                if (count == 1) {
+                    final String name = UIHelper.getMessageDisplayName(messages.get(0));
+                    styledString = new SpannableString(name + ": " + messages.get(0).getBody());
+                    styledString.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), 0);
+                    builder.setContentText(styledString);
+                } else {
+                    builder.setContentText(mXmppConnectionService.getResources().getQuantityString(R.plurals.x_messages, count, count));
+                }
+            }
+        }
+        /** message preview for Android Auto **/
+        for (Message message : messages) {
+            Pair<CharSequence, Boolean> preview = UIHelper.getMessagePreview(mXmppConnectionService, message);
+            // only show user written text
+            if (!preview.second) {
+                uBuilder.addMessage(preview.first.toString());
+                uBuilder.setLatestTimestamp(message.getTimeSent());
+            }
+        }
+    }
+
+    private Message getImage(final Iterable<Message> messages) {
+        Message image = null;
+        for (final Message message : messages) {
+            if (message.getStatus() != Message.STATUS_RECEIVED) {
+                return null;
+            }
+            if (message.getType() != Message.TYPE_TEXT
+                    && message.getTransferable() == null
+                    && message.getEncryption() != Message.ENCRYPTION_PGP
+                    && message.getFileParams().height > 0) {
+                image = message;
+            }
+        }
+        return image;
+    }
+
+    private Message getFirstDownloadableMessage(final Iterable<Message> messages) {
+        for (final Message message : messages) {
+            if (message.getTransferable() != null || (message.getType() == Message.TYPE_TEXT && message.treatAsDownloadable())) {
+                return message;
+            }
+        }
+        return null;
+    }
+
+    private Message getFirstLocationMessage(final Iterable<Message> messages) {
+        for (final Message message : messages) {
+            if (message.isGeoUri()) {
+                return message;
+            }
+        }
+        return null;
+    }
+
+    private CharSequence getMergedBodies(final ArrayList<Message> messages) {
+        final StringBuilder text = new StringBuilder();
+        for (Message message : messages) {
+            if (text.length() != 0) {
+                text.append("\n");
+            }
+            text.append(UIHelper.getMessagePreview(mXmppConnectionService, message).first);
+        }
+        return text.toString();
+    }
+
+    private PendingIntent createShowLocationIntent(final Message message) {
+        Iterable<Intent> intents = GeoHelper.createGeoIntentsFromMessage(mXmppConnectionService, message);
+        for (Intent intent : intents) {
+            if (intent.resolveActivity(mXmppConnectionService.getPackageManager()) != null) {
+                return PendingIntent.getActivity(mXmppConnectionService, generateRequestCode(message.getConversation(), 18), intent, PendingIntent.FLAG_UPDATE_CURRENT);
+            }
+        }
+        return createOpenConversationsIntent();
+    }
+
+    private PendingIntent createContentIntent(final String conversationUuid, final String downloadMessageUuid) {
+        final Intent viewConversationIntent = new Intent(mXmppConnectionService, ConversationsActivity.class);
+        viewConversationIntent.setAction(ConversationsActivity.ACTION_VIEW_CONVERSATION);
+        viewConversationIntent.putExtra(ConversationsActivity.EXTRA_CONVERSATION, conversationUuid);
+        if (downloadMessageUuid != null) {
+            viewConversationIntent.putExtra(ConversationsActivity.EXTRA_DOWNLOAD_UUID, downloadMessageUuid);
+            return PendingIntent.getActivity(mXmppConnectionService,
+                    generateRequestCode(conversationUuid, 8),
+                    viewConversationIntent,
+                    PendingIntent.FLAG_UPDATE_CURRENT);
+        } else {
+            return PendingIntent.getActivity(mXmppConnectionService,
+                    generateRequestCode(conversationUuid, 10),
+                    viewConversationIntent,
+                    PendingIntent.FLAG_UPDATE_CURRENT);
+        }
+    }
+
+    private int generateRequestCode(String uuid, int actionId) {
+        return (actionId * NOTIFICATION_ID_MULTIPLIER) + (uuid.hashCode() % NOTIFICATION_ID_MULTIPLIER);
+    }
+
+    private int generateRequestCode(Conversational conversation, int actionId) {
+        return generateRequestCode(conversation.getUuid(), actionId);
+    }
+
+    private PendingIntent createDownloadIntent(final Message message) {
+        return createContentIntent(message.getConversationUuid(), message.getUuid());
+    }
+
+    private PendingIntent createContentIntent(final Conversational conversation) {
+        return createContentIntent(conversation.getUuid(), null);
+    }
+
+    private PendingIntent createDeleteIntent(Conversation conversation) {
+        final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
+        intent.setAction(XmppConnectionService.ACTION_CLEAR_NOTIFICATION);
+        if (conversation != null) {
+            intent.putExtra("uuid", conversation.getUuid());
+            return PendingIntent.getService(mXmppConnectionService, generateRequestCode(conversation, 20), intent, 0);
+        }
+        return PendingIntent.getService(mXmppConnectionService, 0, intent, 0);
+    }
+
+    private PendingIntent createReplyIntent(Conversation conversation, boolean dismissAfterReply) {
+        final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
+        intent.setAction(XmppConnectionService.ACTION_REPLY_TO_CONVERSATION);
+        intent.putExtra("uuid", conversation.getUuid());
+        intent.putExtra("dismiss_notification", dismissAfterReply);
+        final int id = generateRequestCode(conversation, dismissAfterReply ? 12 : 14);
+        return PendingIntent.getService(mXmppConnectionService, id, intent, 0);
+    }
+
+    private PendingIntent createReadPendingIntent(Conversation conversation) {
+        final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
+        intent.setAction(XmppConnectionService.ACTION_MARK_AS_READ);
+        intent.putExtra("uuid", conversation.getUuid());
+        intent.setPackage(mXmppConnectionService.getPackageName());
+        return PendingIntent.getService(mXmppConnectionService, generateRequestCode(conversation, 16), intent, PendingIntent.FLAG_UPDATE_CURRENT);
+    }
+
+    private PendingIntent createSnoozeIntent(Conversation conversation) {
+        final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
+        intent.setAction(XmppConnectionService.ACTION_SNOOZE);
+        intent.putExtra("uuid", conversation.getUuid());
+        intent.setPackage(mXmppConnectionService.getPackageName());
+        return PendingIntent.getService(mXmppConnectionService, generateRequestCode(conversation, 22), intent, PendingIntent.FLAG_UPDATE_CURRENT);
+    }
+
+    private PendingIntent createTryAgainIntent() {
+        final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
+        intent.setAction(XmppConnectionService.ACTION_TRY_AGAIN);
+        return PendingIntent.getService(mXmppConnectionService, 45, intent, 0);
+    }
+
+    private PendingIntent createDismissErrorIntent() {
+        final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
+        intent.setAction(XmppConnectionService.ACTION_DISMISS_ERROR_NOTIFICATIONS);
+        return PendingIntent.getService(mXmppConnectionService, 69, intent, 0);
+    }
+
+    private boolean wasHighlightedOrPrivate(final Message message) {
+        if (message.getConversation() instanceof Conversation) {
+            Conversation conversation = (Conversation) message.getConversation();
+            final String nick = conversation.getMucOptions().getActualNick();
+            final Pattern highlight = generateNickHighlightPattern(nick);
+            if (message.getBody() == null || nick == null) {
+                return false;
+            }
+            final Matcher m = highlight.matcher(message.getBody());
+            return (m.find() || message.getType() == Message.TYPE_PRIVATE);
+        } else {
+            return false;
+        }
+    }
+
+    public void setOpenConversation(final Conversation conversation) {
+        this.mOpenConversation = conversation;
+    }
+
+    public void setIsInForeground(final boolean foreground) {
+        this.mIsInForeground = foreground;
+    }
+
+    private int getPixel(final int dp) {
+        final DisplayMetrics metrics = mXmppConnectionService.getResources()
+                .getDisplayMetrics();
+        return ((int) (dp * metrics.density));
+    }
+
+    private void markLastNotification() {
+        this.mLastNotification = SystemClock.elapsedRealtime();
+    }
+
+    private boolean inMiniGracePeriod(final Account account) {
+        final int miniGrace = account.getStatus() == Account.State.ONLINE ? Config.MINI_GRACE_PERIOD
+                : Config.MINI_GRACE_PERIOD * 2;
+        return SystemClock.elapsedRealtime() < (this.mLastNotification + miniGrace);
+    }
+
+    public Notification createForegroundNotification() {
+        final Notification.Builder mBuilder = new Notification.Builder(mXmppConnectionService);
+        mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.conversations_foreground_service));
+        if (Compatibility.twentySix() || Config.SHOW_CONNECTED_ACCOUNTS) {
+            List<Account> accounts = mXmppConnectionService.getAccounts();
+            int enabled = 0;
+            int connected = 0;
+            for (Account account : accounts) {
+                if (account.isOnlineAndConnected()) {
+                    connected++;
+                    enabled++;
+                } else if (account.isEnabled()) {
+                    enabled++;
+                }
+            }
+            mBuilder.setContentText(mXmppConnectionService.getString(R.string.connected_accounts, connected, enabled));
+        } else {
+            mBuilder.setContentText(mXmppConnectionService.getString(R.string.touch_to_open_conversations));
+        }
+        mBuilder.setContentIntent(createOpenConversationsIntent());
+        mBuilder.setWhen(0);
+        mBuilder.setPriority(Notification.PRIORITY_LOW);
+        mBuilder.setSmallIcon(R.drawable.ic_link_white_24dp);
+
+        if (Compatibility.twentySix()) {
+            mBuilder.setChannelId("foreground");
+        }
+
+
+        return mBuilder.build();
+    }
+
+    private PendingIntent createOpenConversationsIntent() {
+        return PendingIntent.getActivity(mXmppConnectionService, 0, new Intent(mXmppConnectionService, ConversationsActivity.class), 0);
+    }
+
+    public void updateErrorNotification() {
+        if (Config.SUPPRESS_ERROR_NOTIFICATION) {
+            cancel(ERROR_NOTIFICATION_ID);
+            return;
+        }
+        final List<Account> errors = new ArrayList<>();
+        for (final Account account : mXmppConnectionService.getAccounts()) {
+            if (account.hasErrorStatus() && account.showErrorNotification()) {
+                errors.add(account);
+            }
+        }
+        if (Compatibility.keepForegroundService(mXmppConnectionService)) {
+            notify(FOREGROUND_NOTIFICATION_ID, createForegroundNotification());
+        }
+        final Notification.Builder mBuilder = new Notification.Builder(mXmppConnectionService);
+        if (errors.size() == 0) {
+            cancel(ERROR_NOTIFICATION_ID);
+            return;
+        } else if (errors.size() == 1) {
+            mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.problem_connecting_to_account));
+            mBuilder.setContentText(errors.get(0).getJid().asBareJid().toString());
+        } else {
+            mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.problem_connecting_to_accounts));
+            mBuilder.setContentText(mXmppConnectionService.getString(R.string.touch_to_fix));
+        }
+        mBuilder.addAction(R.drawable.ic_autorenew_white_24dp,
+                mXmppConnectionService.getString(R.string.try_again),
+                createTryAgainIntent());
+        mBuilder.setDeleteIntent(createDismissErrorIntent());
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+            mBuilder.setVisibility(NotificationCompat.VISIBILITY_PRIVATE);
+            mBuilder.setSmallIcon(R.drawable.ic_warning_white_24dp);
+        } else {
+            mBuilder.setSmallIcon(R.drawable.ic_stat_alert_warning);
+        }
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) {
+            mBuilder.setLocalOnly(true);
+        }
+        mBuilder.setPriority(Notification.PRIORITY_LOW);
+        mBuilder.setContentIntent(PendingIntent.getActivity(mXmppConnectionService,
+                145,
+                new Intent(mXmppConnectionService, ManageAccountActivity.class),
+                PendingIntent.FLAG_UPDATE_CURRENT));
+        if (Compatibility.twentySix()) {
+            mBuilder.setChannelId("error");
+        }
+        notify(ERROR_NOTIFICATION_ID, mBuilder.build());
+    }
+
+    public void updateFileAddingNotification(int current, Message message) {
+        Notification.Builder mBuilder = new Notification.Builder(mXmppConnectionService);
+        mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.transcoding_video));
+        mBuilder.setProgress(100, current, false);
+        mBuilder.setSmallIcon(R.drawable.ic_hourglass_empty_white_24dp);
+        mBuilder.setContentIntent(createContentIntent(message.getConversation()));
+        if (Compatibility.twentySix()) {
+            mBuilder.setChannelId("foreground");
+        }
+        Notification notification = mBuilder.build();
+        notify(FOREGROUND_NOTIFICATION_ID, notification);
+    }
+
+    private void notify(String tag, int id, Notification notification) {
+        final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(mXmppConnectionService);
+        try {
+            notificationManager.notify(tag, id, notification);
+        } catch (RuntimeException e) {
+            Log.d(Config.LOGTAG, "unable to make notification", e);
+        }
+    }
+
+    private void notify(int id, Notification notification) {
+        final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(mXmppConnectionService);
+        try {
+            notificationManager.notify(id, notification);
+        } catch (RuntimeException e) {
+            Log.d(Config.LOGTAG, "unable to make notification", e);
+        }
+    }
+
+    private void cancel(int id) {
+        final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(mXmppConnectionService);
+        try {
+            notificationManager.cancel(id);
+        } catch (RuntimeException e) {
+            Log.d(Config.LOGTAG, "unable to cancel notification", e);
+        }
+    }
+
+    private void cancel(String tag, int id) {
+        final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(mXmppConnectionService);
+        try {
+            notificationManager.cancel(tag, id);
+        } catch (RuntimeException e) {
+            Log.d(Config.LOGTAG, "unable to cancel notification", e);
+        }
+    }
 }

src/main/java/eu/siacs/conversations/services/XmppConnectionService.java 🔗

@@ -1,5 +1,6 @@
 package eu.siacs.conversations.services;
 
+import android.Manifest;
 import android.annotation.SuppressLint;
 import android.annotation.TargetApi;
 import android.app.AlarmManager;
@@ -104,6 +105,7 @@ import eu.siacs.conversations.ui.SettingsActivity;
 import eu.siacs.conversations.ui.UiCallback;
 import eu.siacs.conversations.ui.interfaces.OnAvatarPublication;
 import eu.siacs.conversations.ui.interfaces.OnSearchResultsAvailable;
+import eu.siacs.conversations.utils.Compatibility;
 import eu.siacs.conversations.utils.ConversationsFileObserver;
 import eu.siacs.conversations.utils.CryptoHelper;
 import eu.siacs.conversations.utils.ExceptionHelper;
@@ -157,7 +159,6 @@ public class XmppConnectionService extends Service {
     public static final String ACTION_IDLE_PING = "idle_ping";
     public static final String ACTION_FCM_TOKEN_REFRESH = "fcm_token_refresh";
     public static final String ACTION_FCM_MESSAGE_RECEIVED = "fcm_message_received";
-    private static final String ACTION_MERGE_PHONE_CONTACTS = "merge_phone_contacts";
 
     private static final String SETTING_LAST_ACTIVITY_TS = "last_activity_timestamp";
 
@@ -193,10 +194,9 @@ public class XmppConnectionService extends Service {
         @Override
         public void onChange(boolean selfChange) {
             super.onChange(selfChange);
-            Intent intent = new Intent(getApplicationContext(),
-                    XmppConnectionService.class);
-            intent.setAction(ACTION_MERGE_PHONE_CONTACTS);
-            startService(intent);
+            if (restoredFromDatabaseLatch.getCount() == 0) {
+                loadPhoneContacts();
+            }
         }
     };
     private FileBackend fileBackend = new FileBackend(this);
@@ -240,6 +240,7 @@ public class XmppConnectionService extends Service {
     ) {
         @Override
         public void onEvent(int event, String path) {
+            Log.d(Config.LOGTAG,"event "+event+" path="+path);
             markFileDeleted(path);
         }
     };
@@ -569,11 +570,6 @@ public class XmppConnectionService extends Service {
                         resetAllAttemptCounts(true, false);
                     }
                     break;
-                case ACTION_MERGE_PHONE_CONTACTS:
-                    if (restoredFromDatabaseLatch.getCount() == 0) {
-                        loadPhoneContacts();
-                    }
-                    return START_STICKY;
                 case Intent.ACTION_SHUTDOWN:
                     logoutAndSave(true);
                     return START_NOT_STICKY;
@@ -958,6 +954,9 @@ public class XmppConnectionService extends Service {
         Resolver.init(this);
         this.mRandom = new SecureRandom();
         updateMemorizingTrustmanager();
+        if (Compatibility.twentySix()) {
+            mNotificationService.initializeChannels();
+        }
         final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
         final int cacheSize = maxMemory / 8;
         this.mBitmapCache = new LruCache<String, Bitmap>(cacheSize) {
@@ -984,7 +983,10 @@ public class XmppConnectionService extends Service {
 
         restoreFromDatabase();
 
-        getContentResolver().registerContentObserver(ContactsContract.Contacts.CONTENT_URI, true, contactObserver);
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED) {
+            //TODO get this restarted if users gives permission
+            getContentResolver().registerContentObserver(ContactsContract.Contacts.CONTENT_URI, true, contactObserver);
+        }
         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || ContextCompat.checkSelfPermission(this, android.Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
             Log.d(Config.LOGTAG, "starting file observer");
             new Thread(fileObserver::startWatching).start();
@@ -1062,7 +1064,7 @@ public class XmppConnectionService extends Service {
     }
 
     public void toggleForegroundService() {
-        if (mForceForegroundService.get() || (keepForegroundService() && hasEnabledAccounts())) {
+        if (mForceForegroundService.get() || (Compatibility.keepForegroundService(this) && hasEnabledAccounts())) {
             startForeground(NotificationService.FOREGROUND_NOTIFICATION_ID, this.mNotificationService.createForegroundNotification());
             Log.d(Config.LOGTAG, "started foreground service");
         } else {
@@ -1071,14 +1073,11 @@ public class XmppConnectionService extends Service {
         }
     }
 
-    public boolean keepForegroundService() {
-        return getBooleanPreference(SettingsActivity.KEEP_FOREGROUND_SERVICE, R.bool.enable_foreground_service);
-    }
-
     @Override
     public void onTaskRemoved(final Intent rootIntent) {
         super.onTaskRemoved(rootIntent);
-        if (keepForegroundService() || mForceForegroundService.get()) {
+        //TODO check for accounts enabled
+        if ((Compatibility.keepForegroundService(this) && hasEnabledAccounts()) || mForceForegroundService.get()) {
             Log.d(Config.LOGTAG, "ignoring onTaskRemoved because foreground service is activated");
         } else {
             this.logoutAndSave(false);
@@ -1951,6 +1950,7 @@ public class XmppConnectionService extends Service {
 			updateAccountUi();
 			getNotificationService().updateErrorNotification();
 			syncEnabledAccountSetting();
+			toggleForegroundService();
 		}
 	}
 

src/main/java/eu/siacs/conversations/ui/SettingsActivity.java 🔗

@@ -2,6 +2,7 @@ package eu.siacs.conversations.ui;
 
 import android.preference.CheckBoxPreference;
 import android.support.annotation.NonNull;
+import android.support.v4.content.ContextCompat;
 import android.support.v7.app.AlertDialog;
 import android.app.FragmentManager;
 import android.content.DialogInterface;
@@ -406,7 +407,7 @@ public class SettingsActivity extends XmppActivity implements
 	}
 
 	private void startExport() {
-		startService(new Intent(getApplicationContext(), ExportLogsService.class));
+		ContextCompat.startForegroundService(this, new Intent(this, ExportLogsService.class));
 	}
 
 	private void displayToast(final String msg) {

src/main/java/eu/siacs/conversations/ui/SettingsFragment.java 🔗

@@ -11,6 +11,7 @@ import android.widget.ListView;
 
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
+import eu.siacs.conversations.utils.Compatibility;
 
 public class SettingsFragment extends PreferenceFragment {
 
@@ -32,6 +33,7 @@ public class SettingsFragment extends PreferenceFragment {
 				mCategory.removePreference(cleanPrivateStorage);
 			}
 		}
+		Compatibility.removeUnusedPreferences(this);
 
 		if (!TextUtils.isEmpty(page)) {
 			openPreferenceScreen(page);

src/main/java/eu/siacs/conversations/ui/XmppActivity.java 🔗

@@ -532,11 +532,15 @@ public abstract class XmppActivity extends ActionBarActivity {
 	}
 
 	protected void delegateUriPermissionsToService(Uri uri) {
-		Intent intent = new Intent(this,XmppConnectionService.class);
+		Intent intent = new Intent(this, XmppConnectionService.class);
 		intent.setAction(Intent.ACTION_SEND);
 		intent.setData(uri);
 		intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
-		startService(intent);
+		try {
+			startService(intent);
+		} catch (Exception e) {
+			Log.e(Config.LOGTAG,"unable to delegate uri permission",e);
+		}
 	}
 
 	protected void inviteToConversation(Conversation conversation) {

src/main/java/eu/siacs/conversations/utils/Compatibility.java 🔗

@@ -0,0 +1,62 @@
+package eu.siacs.conversations.utils;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Build;
+import android.preference.Preference;
+import android.preference.PreferenceCategory;
+import android.preference.PreferenceGroup;
+import android.preference.PreferenceManager;
+import android.support.annotation.BoolRes;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.ui.SettingsActivity;
+import eu.siacs.conversations.ui.SettingsFragment;
+
+public class Compatibility {
+
+    private static final List<String> UNUSED_SETTINGS_POST_TWENTYSIX = Arrays.asList(
+            SettingsActivity.KEEP_FOREGROUND_SERVICE,
+            "led",
+            "notification_ringtone",
+            "notification_headsup",
+            "vibrate_on_notification");
+    private static final List<String> UNUESD_SETTINGS_PRE_TWENTYSIX = Collections.singletonList("more_notification_settings");
+
+
+    public static boolean twentySix() {
+        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
+    }
+
+    private static boolean getBooleanPreference(Context context, String name, @BoolRes int res) {
+        return getPreferences(context).getBoolean(name, context.getResources().getBoolean(res));
+    }
+
+    private static SharedPreferences getPreferences(final Context context) {
+        return PreferenceManager.getDefaultSharedPreferences(context);
+    }
+
+    public static boolean keepForegroundService(Context context) {
+        return twentySix() || getBooleanPreference(context, SettingsActivity.KEEP_FOREGROUND_SERVICE, R.bool.enable_foreground_service);
+    }
+
+    public static void removeUnusedPreferences(SettingsFragment settingsFragment) {
+        List<PreferenceCategory> categories = Arrays.asList(
+                (PreferenceCategory) settingsFragment.findPreference("notification_category"),
+                (PreferenceCategory) settingsFragment.findPreference("other_expert_category"));
+        for (String key : (twentySix() ? UNUSED_SETTINGS_POST_TWENTYSIX : UNUESD_SETTINGS_PRE_TWENTYSIX)) {
+            Preference preference = settingsFragment.findPreference(key);
+            if (preference != null) {
+                for (PreferenceCategory category : categories) {
+                    if (category != null) {
+                        category.removePreference(preference);
+                    }
+                }
+            }
+        }
+    }
+}

src/main/java/eu/siacs/conversations/utils/ConversationsFileObserver.java 🔗

@@ -2,12 +2,15 @@ package eu.siacs.conversations.utils;
 
 
 import android.os.FileObserver;
+import android.util.Log;
 
 import java.io.File;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Stack;
 
+import eu.siacs.conversations.Config;
+
 /**
  * Copyright (C) 2012 Bartek Przybylski
  * Copyright (C) 2015 ownCloud Inc.
@@ -19,7 +22,7 @@ public abstract class ConversationsFileObserver {
     private final String path;
     private final List<SingleFileObserver> mObservers = new ArrayList<>();
 
-    public ConversationsFileObserver(String path) {
+    protected ConversationsFileObserver(String path) {
         this.path = path;
     }
 
@@ -83,13 +86,17 @@ public abstract class ConversationsFileObserver {
     private class SingleFileObserver extends FileObserver {
         private final String path;
 
-        public SingleFileObserver(String path, int mask) {
+        SingleFileObserver(String path, int mask) {
             super(path, mask);
             this.path = path;
         }
 
         @Override
         public void onEvent(int event, String filename) {
+            if (filename == null) {
+                Log.d(Config.LOGTAG,"ignored file event with NULL filename (event="+event+")");
+                return;
+            }
             ConversationsFileObserver.this.onEvent(event, path+'/'+filename);
         }
 

src/main/res/values/strings.xml 🔗

@@ -727,4 +727,15 @@
     <string name="conference_destroyed">This group chat has been destroyed</string>
     <string name="phone_book">Address book</string>
     <string name="unable_to_save_recording">Unable to save recording</string>
+    <string name="foreground_service_channel_name">Foreground service</string>
+    <string name="foreground_service_channel_description">This notification category is used to display a permanent notification indicating that Conversations is running.</string>
+    <string name="notification_group_status_information">Status Information</string>
+    <string name="error_channel_name">Connectivity Problems</string>
+    <string name="error_channel_description">This notification category is used to display a notification in case there is a problem connecting to an account.</string>
+    <string name="notification_group_messages">Messages</string>
+    <string name="messages_channel_name">Messages</string>
+    <string name="silent_messages_channel_name">Silent messages</string>
+    <string name="silent_messages_channel_description">This notification group is used to display notifications that should not trigger any sound. For example when being active on another device (Grace Period).</string>
+    <string name="pref_more_notification_settings">Notification Settings</string>
+    <string name="pref_more_notification_settings_summary">Importance, Sound, Vibrate</string>
 </resources>

src/main/res/xml/preferences.xml 🔗

@@ -1,6 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
-<PreferenceScreen
-    xmlns:android="http://schemas.android.com/apk/res/android"
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
     android:background="?attr/color_background_secondary"
     android:key="main_screen">
 
@@ -10,11 +9,10 @@
         <PreferenceScreen
             android:key="huawei"
             android:summary="@string/huawei_protected_apps_summary"
-            android:title="@string/huawei_protected_apps"
-            >
+            android:title="@string/huawei_protected_apps">
             <intent
                 android:targetClass="com.huawei.systemmanager.optimize.process.ProtectActivity"
-                android:targetPackage="com.huawei.systemmanager"/>
+                android:targetPackage="com.huawei.systemmanager" />
         </PreferenceScreen>
     </PreferenceCategory>
     <PreferenceCategory android:title="@string/pref_privacy">
@@ -24,62 +22,77 @@
             android:entryValues="@array/omemo_setting_entry_values"
             android:key="omemo"
             android:summary="@string/pref_omemo_setting_summary_default_on"
-            android:title="@string/pref_omemo_setting"
-            />
+            android:title="@string/pref_omemo_setting" />
         <CheckBoxPreference
             android:defaultValue="@bool/confirm_messages"
             android:key="confirm_messages"
             android:summary="@string/pref_confirm_messages_summary"
-            android:title="@string/pref_confirm_messages"/>
+            android:title="@string/pref_confirm_messages" />
 
         <CheckBoxPreference
             android:defaultValue="@bool/chat_states"
             android:key="chat_states"
             android:summary="@string/pref_chat_states_summary"
-            android:title="@string/pref_chat_states"/>
+            android:title="@string/pref_chat_states" />
         <CheckBoxPreference
             android:defaultValue="@bool/last_activity"
             android:key="last_activity"
             android:summary="@string/pref_broadcast_last_activity_summary"
-            android:title="@string/pref_broadcast_last_activity"/>
+            android:title="@string/pref_broadcast_last_activity" />
     </PreferenceCategory>
-    <PreferenceCategory android:title="@string/pref_notification_settings">
+    <PreferenceCategory
+        android:title="@string/pref_notification_settings"
+        android:key="notification_category">
         <CheckBoxPreference
             android:defaultValue="@bool/show_notification"
             android:key="show_notification"
             android:summary="@string/pref_notifications_summary"
-            android:title="@string/pref_notifications"/>
+            android:title="@string/pref_notifications" />
         <CheckBoxPreference
             android:defaultValue="@bool/notifications_from_strangers"
             android:dependency="show_notification"
             android:key="notifications_from_strangers"
             android:summary="@string/pref_notifications_from_strangers_summary"
-            android:title="@string/pref_notifications_from_strangers"/>
+            android:title="@string/pref_notifications_from_strangers" />
+        <PreferenceScreen
+            android:key="more_notification_settings"
+            android:dependency="show_notification"
+            android:summary="@string/pref_more_notification_settings_summary"
+            android:title="@string/pref_more_notification_settings">
+            <intent android:action="android.settings.CHANNEL_NOTIFICATION_SETTINGS">
+                <extra
+                    android:name="android.provider.extra.APP_PACKAGE"
+                    android:value="@string/applicationId" />
+                <extra
+                    android:name="android.provider.extra.CHANNEL_ID"
+                    android:value="messages" />
+            </intent>
+        </PreferenceScreen>
         <CheckBoxPreference
             android:defaultValue="@bool/headsup_notifications"
             android:dependency="show_notification"
             android:key="notification_headsup"
             android:summary="@string/pref_headsup_notifications_summary"
-            android:title="@string/pref_headsup_notifications"/>
+            android:title="@string/pref_headsup_notifications" />
         <CheckBoxPreference
             android:defaultValue="@bool/vibrate_on_notification"
             android:dependency="show_notification"
             android:key="vibrate_on_notification"
             android:summary="@string/pref_vibrate_summary"
-            android:title="@string/pref_vibrate"/>
+            android:title="@string/pref_vibrate" />
         <CheckBoxPreference
             android:defaultValue="@bool/led"
             android:dependency="show_notification"
             android:key="led"
             android:summary="@string/pref_led_summary"
-            android:title="@string/pref_led"/>
+            android:title="@string/pref_led" />
         <RingtonePreference
             android:defaultValue="@string/notification_ringtone"
             android:dependency="show_notification"
             android:key="notification_ringtone"
             android:ringtoneType="notification"
             android:summary="@string/pref_sound_summary"
-            android:title="@string/pref_sound"/>
+            android:title="@string/pref_sound" />
         <PreferenceScreen
             android:dependency="show_notification"
             android:key="quiet_hours"
@@ -91,25 +104,25 @@
                 android:targetPackage="@string/applicationId">
                 <extra
                     android:name="page"
-                    android:value="quiet_hours"/>
+                    android:value="quiet_hours" />
             </intent>
             <CheckBoxPreference
                 android:defaultValue="@bool/enable_quiet_hours"
                 android:key="enable_quiet_hours"
                 android:summary="@string/pref_quiet_hours_summary"
-                android:title="@string/title_pref_enable_quiet_hours"/>
+                android:title="@string/title_pref_enable_quiet_hours" />
             <eu.siacs.conversations.ui.TimePreference
                 android:dependency="enable_quiet_hours"
                 android:key="quiet_hours_start"
                 android:negativeButtonText="@string/cancel"
                 android:positiveButtonText="@string/set"
-                android:title="@string/title_pref_quiet_hours_start_time"/>
+                android:title="@string/title_pref_quiet_hours_start_time" />
             <eu.siacs.conversations.ui.TimePreference
                 android:dependency="enable_quiet_hours"
                 android:key="quiet_hours_end"
                 android:negativeButtonText="@string/cancel"
                 android:positiveButtonText="@string/set"
-                android:title="@string/title_pref_quiet_hours_end_time"/>
+                android:title="@string/title_pref_quiet_hours_end_time" />
         </PreferenceScreen>
         <ListPreference
             android:defaultValue="@integer/grace_period"
@@ -118,8 +131,7 @@
             android:entryValues="@array/grace_periods_values"
             android:key="grace_period_length"
             android:summary="@string/pref_notification_grace_period_summary"
-            android:title="@string/pref_notification_grace_period"
-            />
+            android:title="@string/pref_notification_grace_period" />
     </PreferenceCategory>
     <PreferenceCategory
         android:key="attachments"
@@ -130,24 +142,24 @@
             android:entryValues="@array/filesizes_values"
             android:key="auto_accept_file_size"
             android:summary="@string/pref_accept_files_summary"
-            android:title="@string/pref_accept_files"/>
+            android:title="@string/pref_accept_files" />
         <ListPreference
             android:defaultValue="@string/picture_compression"
             android:entries="@array/picture_compression_entries"
             android:entryValues="@array/picture_compression_values"
             android:key="picture_compression"
             android:summary="@string/pref_picture_compression_summary"
-            android:title="@string/pref_picture_compression"/>
+            android:title="@string/pref_picture_compression" />
         <CheckBoxPreference
             android:defaultValue="@bool/return_to_previous"
             android:key="return_to_previous"
             android:summary="@string/pref_return_to_previous_summary"
-            android:title="@string/pref_return_to_previous"/>
+            android:title="@string/pref_return_to_previous" />
         <CheckBoxPreference
             android:defaultValue="@bool/use_share_location_plugin"
             android:key="use_share_location_plugin"
             android:summary="@string/pref_use_share_location_plugin_summary"
-            android:title="@string/pref_use_share_location_plugin"/>
+            android:title="@string/pref_use_share_location_plugin" />
     </PreferenceCategory>
     <PreferenceCategory android:title="@string/pref_ui_options">
         <ListPreference
@@ -156,24 +168,24 @@
             android:entryValues="@array/themes_values"
             android:key="theme"
             android:summary="@string/pref_theme_options_summary"
-            android:title="@string/pref_theme_options"/>
+            android:title="@string/pref_theme_options" />
         <CheckBoxPreference
             android:defaultValue="@bool/use_green_background"
             android:key="use_green_background"
             android:summary="@string/pref_use_green_background_summary"
-            android:title="@string/pref_use_green_background"/>
+            android:title="@string/pref_use_green_background" />
         <ListPreference
             android:defaultValue="@string/default_font_size"
             android:entries="@array/font_size_entries"
             android:entryValues="@array/font_size_entry_values"
             android:key="font_size"
             android:summary="@string/pref_font_size_summary"
-            android:title="@string/pref_font_size"/>
+            android:title="@string/pref_font_size" />
         <CheckBoxPreference
             android:defaultValue="@bool/send_button_status"
             android:key="send_button_status"
             android:summary="@string/pref_use_send_button_to_indicate_status_summary"
-            android:title="@string/pref_use_send_button_to_indicate_status"/>
+            android:title="@string/pref_use_send_button_to_indicate_status" />
         <ListPreference
             android:defaultValue="@string/quick_action"
             android:dialogTitle="@string/choose_quick_action"
@@ -181,12 +193,12 @@
             android:entryValues="@array/quick_action_values"
             android:key="quick_action"
             android:summary="@string/pref_quick_action_summary"
-            android:title="@string/pref_quick_action"/>
+            android:title="@string/pref_quick_action" />
         <CheckBoxPreference
             android:defaultValue="@bool/show_dynamic_tags"
             android:key="show_dynamic_tags"
             android:summary="@string/pref_show_dynamic_tags_summary"
-            android:title="@string/pref_show_dynamic_tags"/>
+            android:title="@string/pref_show_dynamic_tags" />
     </PreferenceCategory>
     <PreferenceCategory
         android:key="advanced"
@@ -201,7 +213,7 @@
                 android:targetPackage="@string/applicationId">
                 <extra
                     android:name="page"
-                    android:value="expert"/>
+                    android:value="expert" />
             </intent>
             <PreferenceCategory
                 android:key="security_options"
@@ -210,43 +222,43 @@
                     android:defaultValue="@bool/btbv"
                     android:key="btbv"
                     android:summary="@string/pref_blind_trust_before_verification_summary"
-                    android:title="@string/pref_blind_trust_before_verification"/>
+                    android:title="@string/pref_blind_trust_before_verification" />
                 <ListPreference
                     android:defaultValue="@integer/automatic_message_deletion"
                     android:key="automatic_message_deletion"
                     android:summary="@string/pref_automatically_delete_messages_description"
-                    android:title="@string/pref_automatically_delete_messages"/>
+                    android:title="@string/pref_automatically_delete_messages" />
                 <CheckBoxPreference
                     android:defaultValue="@bool/dont_trust_system_cas"
                     android:key="dont_trust_system_cas"
                     android:summary="@string/pref_dont_trust_system_cas_summary"
-                    android:title="@string/pref_dont_trust_system_cas_title"/>
+                    android:title="@string/pref_dont_trust_system_cas_title" />
                 <CheckBoxPreference
                     android:defaultValue="@bool/validate_hostname"
                     android:key="validate_hostname"
                     android:summary="@string/pref_validate_hostname_summary"
-                    android:title="@string/pref_validate_hostname"/>
+                    android:title="@string/pref_validate_hostname" />
                 <Preference
                     android:key="remove_trusted_certificates"
                     android:summary="@string/pref_remove_trusted_certificates_summary"
-                    android:title="@string/pref_remove_trusted_certificates_title"/>
+                    android:title="@string/pref_remove_trusted_certificates_title" />
                 <CheckBoxPreference
                     android:defaultValue="@bool/allow_message_correction"
                     android:key="allow_message_correction"
                     android:summary="@string/pref_allow_message_correction_summary"
-                    android:title="@string/pref_allow_message_correction"/>
+                    android:title="@string/pref_allow_message_correction" />
                 <Preference
                     android:key="clean_cache"
                     android:summary="@string/pref_clean_cache_summary"
-                    android:title="@string/pref_clean_cache"/>
+                    android:title="@string/pref_clean_cache" />
                 <Preference
                     android:key="clean_private_storage"
                     android:summary="@string/pref_clean_private_storage_summary"
-                    android:title="@string/pref_clean_private_storage"/>
+                    android:title="@string/pref_clean_private_storage" />
                 <Preference
                     android:key="delete_omemo_identities"
                     android:summary="@string/pref_delete_omemo_identities_summary"
-                    android:title="@string/pref_delete_omemo_identities"/>
+                    android:title="@string/pref_delete_omemo_identities" />
             </PreferenceCategory>
             <PreferenceCategory
                 android:key="connection_options"
@@ -255,34 +267,34 @@
                     android:defaultValue="@bool/use_tor"
                     android:key="use_tor"
                     android:summary="@string/pref_use_tor_summary"
-                    android:title="@string/pref_use_tor"/>
+                    android:title="@string/pref_use_tor" />
                 <CheckBoxPreference
                     android:defaultValue="@bool/show_connection_options"
                     android:key="show_connection_options"
                     android:summary="@string/pref_show_connection_options_summary"
-                    android:title="@string/pref_show_connection_options"/>
+                    android:title="@string/pref_show_connection_options" />
             </PreferenceCategory>
             <PreferenceCategory android:title="@string/pref_input_options">
                 <CheckBoxPreference
                     android:defaultValue="@bool/start_searching"
                     android:key="start_searching"
                     android:summary="@string/pref_start_search_summary"
-                    android:title="@string/pref_start_search"/>
+                    android:title="@string/pref_start_search" />
                 <CheckBoxPreference
                     android:defaultValue="@bool/enter_is_send"
                     android:key="enter_is_send"
                     android:summary="@string/pref_enter_is_send_summary"
-                    android:title="@string/pref_enter_is_send"/>
+                    android:title="@string/pref_enter_is_send" />
                 <CheckBoxPreference
                     android:defaultValue="@bool/display_enter_key"
                     android:key="display_enter_key"
                     android:summary="@string/pref_display_enter_key_summary"
-                    android:title="@string/pref_display_enter_key"/>
+                    android:title="@string/pref_display_enter_key" />
                 <CheckBoxPreference
                     android:defaultValue="@bool/scroll_to_bottom"
                     android:key="scroll_to_bottom"
                     android:summary="@string/pref_scroll_to_bottom_summary"
-                    android:title="@string/pref_scroll_to_bottom"/>
+                    android:title="@string/pref_scroll_to_bottom" />
             </PreferenceCategory>
             <PreferenceCategory android:title="@string/pref_presence_settings">
                 <CheckBoxPreference
@@ -290,47 +302,48 @@
                     android:disableDependentsState="true"
                     android:key="manually_change_presence"
                     android:summary="@string/pref_manually_change_presence_summary"
-                    android:title="@string/pref_manually_change_presence"/>
+                    android:title="@string/pref_manually_change_presence" />
                 <CheckBoxPreference
                     android:defaultValue="@bool/away_when_screen_off"
                     android:dependency="manually_change_presence"
                     android:key="away_when_screen_off"
                     android:summary="@string/pref_away_when_screen_off_summary"
-                    android:title="@string/pref_away_when_screen_off"/>
+                    android:title="@string/pref_away_when_screen_off" />
                 <CheckBoxPreference
                     android:defaultValue="@bool/dnd_on_silent_mode"
                     android:dependency="manually_change_presence"
                     android:key="dnd_on_silent_mode"
                     android:summary="@string/pref_dnd_on_silent_mode_summary"
-                    android:title="@string/pref_dnd_on_silent_mode"/>
+                    android:title="@string/pref_dnd_on_silent_mode" />
                 <CheckBoxPreference
                     android:defaultValue="@bool/treat_vibrate_as_silent"
                     android:dependency="dnd_on_silent_mode"
                     android:key="treat_vibrate_as_silent"
                     android:summary="@string/pref_treat_vibrate_as_dnd_summary"
-                    android:title="@string/pref_treat_vibrate_as_silent"/>
+                    android:title="@string/pref_treat_vibrate_as_silent" />
             </PreferenceCategory>
-            <PreferenceCategory android:title="@string/pref_expert_options_other">
+            <PreferenceCategory
+                android:key="other_expert_category"
+                android:title="@string/pref_expert_options_other">
                 <CheckBoxPreference
                     android:defaultValue="@bool/autojoin"
                     android:key="autojoin"
                     android:summary="@string/pref_autojoin_summary"
-                    android:title="@string/pref_autojoin"
-                    />
+                    android:title="@string/pref_autojoin" />
                 <CheckBoxPreference
                     android:defaultValue="@bool/indicate_received"
                     android:key="indicate_received"
                     android:summary="@string/pref_use_indicate_received_summary"
-                    android:title="@string/pref_use_indicate_received"/>
+                    android:title="@string/pref_use_indicate_received" />
                 <CheckBoxPreference
                     android:defaultValue="@bool/enable_foreground_service"
                     android:key="enable_foreground_service"
                     android:summary="@string/pref_keep_foreground_service_summary"
-                    android:title="@string/pref_keep_foreground_service"/>
+                    android:title="@string/pref_keep_foreground_service" />
                 <Preference
                     android:key="export_logs"
                     android:summary="@string/pref_export_logs_summary"
-                    android:title="@string/pref_export_logs"/>
+                    android:title="@string/pref_export_logs" />
             </PreferenceCategory>
         </PreferenceScreen>
 
@@ -338,9 +351,9 @@
             android:defaultValue="@bool/never_send"
             android:key="never_send"
             android:summary="@string/pref_never_send_crash_summary"
-            android:title="@string/pref_never_send_crash"/>
+            android:title="@string/pref_never_send_crash" />
     </PreferenceCategory>
     <eu.siacs.conversations.ui.AboutPreference
         android:summary="@string/pref_about_conversations_summary"
-        android:title="@string/title_activity_about"/>
+        android:title="@string/title_activity_about" />
 </PreferenceScreen>