~singpolyma/cheogram-android

676d31f60607656b064e8ada59f314bca75a3c48 — Daniel Gultsch 5 years ago d5b50d1
initial work toward api 26+

* introduce notification channels
* always use foreground service on 26+
M build.gradle => build.gradle +3 -3
@@ 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

M gradle.properties => gradle.properties +0 -1
@@ 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

M gradle/wrapper/gradle-wrapper.properties => gradle/wrapper/gradle-wrapper.properties +1 -1
@@ 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

M src/main/AndroidManifest.xml => src/main/AndroidManifest.xml +1 -0
@@ 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"

M src/main/java/eu/siacs/conversations/services/BarcodeProvider.java => src/main/java/eu/siacs/conversations/services/BarcodeProvider.java +1 -1
@@ 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;
				}

M src/main/java/eu/siacs/conversations/services/ContactChooserTargetService.java => src/main/java/eu/siacs/conversations/services/ContactChooserTargetService.java +1 -1
@@ 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 {

M src/main/java/eu/siacs/conversations/services/EventReceiver.java => src/main/java/eu/siacs/conversations/services/EventReceiver.java +10 -11
@@ 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);
	}


M src/main/java/eu/siacs/conversations/services/ExportLogsService.java => src/main/java/eu/siacs/conversations/services/ExportLogsService.java +6 -9
@@ 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;
	}

M src/main/java/eu/siacs/conversations/services/NotificationService.java => src/main/java/eu/siacs/conversations/services/NotificationService.java +868 -804
@@ 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);
        }
    }
}

M src/main/java/eu/siacs/conversations/services/XmppConnectionService.java => src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +17 -17
@@ 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();
		}
	}


M src/main/java/eu/siacs/conversations/ui/SettingsActivity.java => src/main/java/eu/siacs/conversations/ui/SettingsActivity.java +2 -1
@@ 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) {

M src/main/java/eu/siacs/conversations/ui/SettingsFragment.java => src/main/java/eu/siacs/conversations/ui/SettingsFragment.java +2 -0
@@ 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);

M src/main/java/eu/siacs/conversations/ui/XmppActivity.java => src/main/java/eu/siacs/conversations/ui/XmppActivity.java +6 -2
@@ 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) {

A src/main/java/eu/siacs/conversations/utils/Compatibility.java => src/main/java/eu/siacs/conversations/utils/Compatibility.java +62 -0
@@ 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);
                    }
                }
            }
        }
    }
}

M src/main/java/eu/siacs/conversations/utils/ConversationsFileObserver.java => src/main/java/eu/siacs/conversations/utils/ConversationsFileObserver.java +9 -2
@@ 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);
        }


M src/main/res/values/strings.xml => src/main/res/values/strings.xml +11 -0
@@ 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>

M src/main/res/xml/preferences.xml => src/main/res/xml/preferences.xml +74 -61
@@ 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>