diff --git a/build.gradle b/build.gradle
index e7708a795..200799c43 100644
--- a/build.gradle
+++ b/build.gradle
@@ -60,13 +60,13 @@ ext {
}
android {
- compileSdkVersion 27
+ compileSdkVersion 28
defaultConfig {
minSdkVersion 19
- targetSdkVersion 25
+ targetSdkVersion 28
versionCode 283
- versionName "2.2.9"
+ versionName "2.3.0-alpha"
archivesBaseName += "-$versionName"
applicationId "eu.siacs.conversations"
resValue "string", "applicationId", applicationId
diff --git a/gradle.properties b/gradle.properties
index 1c9306fb2..45bbf213d 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -13,4 +13,3 @@
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
org.gradle.jvmargs=-Xmx2048M
-org.gradle.configureondemand=false
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index bd24854fe..ef9b31b2e 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-4.9-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.10-all.zip
diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml
index 8f4d4a72f..86560f8be 100644
--- a/src/main/AndroidManifest.xml
+++ b/src/main/AndroidManifest.xml
@@ -16,6 +16,7 @@
+
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 chooserTargets = new ArrayList<>();
try {
diff --git a/src/main/java/eu/siacs/conversations/services/EventReceiver.java b/src/main/java/eu/siacs/conversations/services/EventReceiver.java
index 9b44bf2f7..fe8f469a5 100644
--- a/src/main/java/eu/siacs/conversations/services/EventReceiver.java
+++ b/src/main/java/eu/siacs/conversations/services/EventReceiver.java
@@ -3,38 +3,37 @@ package eu.siacs.conversations.services;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
-import android.content.SharedPreferences;
import android.preference.PreferenceManager;
+import android.support.v4.content.ContextCompat;
import android.util.Log;
import eu.siacs.conversations.Config;
-import eu.siacs.conversations.persistance.DatabaseBackend;
public class EventReceiver extends BroadcastReceiver {
public static final String SETTING_ENABLED_ACCOUNTS = "enabled_accounts";
@Override
- public void onReceive(Context context, Intent intent) {
- Intent mIntentForService = new Intent(context, XmppConnectionService.class);
- if (intent.getAction() != null) {
- mIntentForService.setAction(intent.getAction());
+ public void onReceive(final Context context, final Intent originalIntent) {
+ final Intent intentForService = new Intent(context, XmppConnectionService.class);
+ if (originalIntent.getAction() != null) {
+ intentForService.setAction(originalIntent.getAction());
} else {
- mIntentForService.setAction("other");
+ intentForService.setAction("other");
}
- final String action = intent.getAction();
+ final String action = originalIntent.getAction();
if (action.equals("ui") || hasEnabledAccounts(context)) {
try {
- context.startService(mIntentForService);
+ ContextCompat.startForegroundService(context, intentForService);
} catch (RuntimeException e) {
Log.d(Config.LOGTAG,"EventReceiver was unable to start service");
}
} else {
- Log.d(Config.LOGTAG,"EventReceiver ignored action "+mIntentForService.getAction());
+ Log.d(Config.LOGTAG,"EventReceiver ignored action "+intentForService.getAction());
}
}
- public static boolean hasEnabledAccounts(Context context) {
+ public static boolean hasEnabledAccounts(final Context context) {
return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTING_ENABLED_ACCOUNTS,true);
}
diff --git a/src/main/java/eu/siacs/conversations/services/ExportLogsService.java b/src/main/java/eu/siacs/conversations/services/ExportLogsService.java
index ab84914f2..efa229fce 100644
--- a/src/main/java/eu/siacs/conversations/services/ExportLogsService.java
+++ b/src/main/java/eu/siacs/conversations/services/ExportLogsService.java
@@ -42,15 +42,12 @@ public class ExportLogsService extends Service {
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (running.compareAndSet(false, true)) {
- new Thread(new Runnable() {
- @Override
- public void run() {
- export();
- stopForeground(true);
- running.set(false);
- stopSelf();
- }
- }).start();
+ new Thread(() -> {
+ export();
+ stopForeground(true);
+ running.set(false);
+ stopSelf();
+ }).start();
}
return START_NOT_STICKY;
}
diff --git a/src/main/java/eu/siacs/conversations/services/NotificationService.java b/src/main/java/eu/siacs/conversations/services/NotificationService.java
index 88a0573d1..ad565e897 100644
--- a/src/main/java/eu/siacs/conversations/services/NotificationService.java
+++ b/src/main/java/eu/siacs/conversations/services/NotificationService.java
@@ -1,16 +1,23 @@
package eu.siacs.conversations.services;
import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationChannelGroup;
+import android.app.NotificationManager;
import android.app.PendingIntent;
+import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Typeface;
+import android.media.AudioAttributes;
+import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Build;
import android.os.SystemClock;
import android.preference.PreferenceManager;
+import android.support.annotation.RequiresApi;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.NotificationCompat.BigPictureStyle;
import android.support.v4.app.NotificationCompat.Builder;
@@ -25,7 +32,6 @@ import android.util.Log;
import android.util.Pair;
import java.io.File;
-import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Calendar;
@@ -49,813 +55,871 @@ import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.ui.ConversationsActivity;
import eu.siacs.conversations.ui.ManageAccountActivity;
import eu.siacs.conversations.ui.TimePreference;
+import eu.siacs.conversations.utils.Compatibility;
import eu.siacs.conversations.utils.GeoHelper;
import eu.siacs.conversations.utils.UIHelper;
import eu.siacs.conversations.xmpp.XmppConnection;
public class NotificationService {
- public static final Object CATCHUP_LOCK = new Object();
-
- private static final String CONVERSATIONS_GROUP = "eu.siacs.conversations";
- private final XmppConnectionService mXmppConnectionService;
-
- private final LinkedHashMap> 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 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> it = mBacklogMessageCounter.entrySet().iterator(); it.hasNext(); ) {
- Map.Entry 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 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 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 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> 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 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 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 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 messages) {
- try {
- final Bitmap bitmap = mXmppConnectionService.getFileBackend()
- .getThumbnail(message, getPixel(288), false);
- final ArrayList 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 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 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 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 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 messages) {
- for (final Message message : messages) {
- if (message.isGeoUri()) {
- return message;
- }
- }
- return null;
- }
-
- private CharSequence getMergedBodies(final ArrayList 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 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 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 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> notifications = new LinkedHashMap<>();
+ private final HashMap mBacklogMessageCounter = new HashMap<>();
+ private Conversation mOpenConversation;
+ private boolean mIsInForeground;
+ private long mLastNotification;
+
+ NotificationService(final XmppConnectionService service) {
+ this.mXmppConnectionService = service;
+ }
+
+ private static boolean displaySnoozeAction(List 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> it = mBacklogMessageCounter.entrySet().iterator(); it.hasNext(); ) {
+ Map.Entry 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 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 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 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> 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 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 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 messages) {
+ try {
+ final Bitmap bitmap = mXmppConnectionService.getFileBackend()
+ .getThumbnail(message, getPixel(288), false);
+ final ArrayList 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 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 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 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 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 messages) {
+ for (final Message message : messages) {
+ if (message.isGeoUri()) {
+ return message;
+ }
+ }
+ return null;
+ }
+
+ private CharSequence getMergedBodies(final ArrayList 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 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 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 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);
+ }
+ }
}
diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java
index 7c6a95f15..c550cbb9c 100644
--- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java
+++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java
@@ -1,5 +1,6 @@
package eu.siacs.conversations.services;
+import android.Manifest;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.AlarmManager;
@@ -104,6 +105,7 @@ import eu.siacs.conversations.ui.SettingsActivity;
import eu.siacs.conversations.ui.UiCallback;
import eu.siacs.conversations.ui.interfaces.OnAvatarPublication;
import eu.siacs.conversations.ui.interfaces.OnSearchResultsAvailable;
+import eu.siacs.conversations.utils.Compatibility;
import eu.siacs.conversations.utils.ConversationsFileObserver;
import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.utils.ExceptionHelper;
@@ -157,7 +159,6 @@ public class XmppConnectionService extends Service {
public static final String ACTION_IDLE_PING = "idle_ping";
public static final String ACTION_FCM_TOKEN_REFRESH = "fcm_token_refresh";
public static final String ACTION_FCM_MESSAGE_RECEIVED = "fcm_message_received";
- private static final String ACTION_MERGE_PHONE_CONTACTS = "merge_phone_contacts";
private static final String SETTING_LAST_ACTIVITY_TS = "last_activity_timestamp";
@@ -193,10 +194,9 @@ public class XmppConnectionService extends Service {
@Override
public void onChange(boolean selfChange) {
super.onChange(selfChange);
- Intent intent = new Intent(getApplicationContext(),
- XmppConnectionService.class);
- intent.setAction(ACTION_MERGE_PHONE_CONTACTS);
- startService(intent);
+ if (restoredFromDatabaseLatch.getCount() == 0) {
+ loadPhoneContacts();
+ }
}
};
private FileBackend fileBackend = new FileBackend(this);
@@ -240,6 +240,7 @@ public class XmppConnectionService extends Service {
) {
@Override
public void onEvent(int event, String path) {
+ Log.d(Config.LOGTAG,"event "+event+" path="+path);
markFileDeleted(path);
}
};
@@ -569,11 +570,6 @@ public class XmppConnectionService extends Service {
resetAllAttemptCounts(true, false);
}
break;
- case ACTION_MERGE_PHONE_CONTACTS:
- if (restoredFromDatabaseLatch.getCount() == 0) {
- loadPhoneContacts();
- }
- return START_STICKY;
case Intent.ACTION_SHUTDOWN:
logoutAndSave(true);
return START_NOT_STICKY;
@@ -958,6 +954,9 @@ public class XmppConnectionService extends Service {
Resolver.init(this);
this.mRandom = new SecureRandom();
updateMemorizingTrustmanager();
+ if (Compatibility.twentySix()) {
+ mNotificationService.initializeChannels();
+ }
final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
final int cacheSize = maxMemory / 8;
this.mBitmapCache = new LruCache(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();
}
}
diff --git a/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java b/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java
index 653a55032..ebdff2c00 100644
--- a/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java
@@ -2,6 +2,7 @@ package eu.siacs.conversations.ui;
import android.preference.CheckBoxPreference;
import android.support.annotation.NonNull;
+import android.support.v4.content.ContextCompat;
import android.support.v7.app.AlertDialog;
import android.app.FragmentManager;
import android.content.DialogInterface;
@@ -406,7 +407,7 @@ public class SettingsActivity extends XmppActivity implements
}
private void startExport() {
- startService(new Intent(getApplicationContext(), ExportLogsService.class));
+ ContextCompat.startForegroundService(this, new Intent(this, ExportLogsService.class));
}
private void displayToast(final String msg) {
diff --git a/src/main/java/eu/siacs/conversations/ui/SettingsFragment.java b/src/main/java/eu/siacs/conversations/ui/SettingsFragment.java
index b3d3f09bc..9378d82b7 100644
--- a/src/main/java/eu/siacs/conversations/ui/SettingsFragment.java
+++ b/src/main/java/eu/siacs/conversations/ui/SettingsFragment.java
@@ -11,6 +11,7 @@ import android.widget.ListView;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
+import eu.siacs.conversations.utils.Compatibility;
public class SettingsFragment extends PreferenceFragment {
@@ -32,6 +33,7 @@ public class SettingsFragment extends PreferenceFragment {
mCategory.removePreference(cleanPrivateStorage);
}
}
+ Compatibility.removeUnusedPreferences(this);
if (!TextUtils.isEmpty(page)) {
openPreferenceScreen(page);
diff --git a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java
index 3b747187c..dce1826e6 100644
--- a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java
@@ -532,11 +532,15 @@ public abstract class XmppActivity extends ActionBarActivity {
}
protected void delegateUriPermissionsToService(Uri uri) {
- Intent intent = new Intent(this,XmppConnectionService.class);
+ Intent intent = new Intent(this, XmppConnectionService.class);
intent.setAction(Intent.ACTION_SEND);
intent.setData(uri);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
- startService(intent);
+ try {
+ startService(intent);
+ } catch (Exception e) {
+ Log.e(Config.LOGTAG,"unable to delegate uri permission",e);
+ }
}
protected void inviteToConversation(Conversation conversation) {
diff --git a/src/main/java/eu/siacs/conversations/utils/Compatibility.java b/src/main/java/eu/siacs/conversations/utils/Compatibility.java
new file mode 100644
index 000000000..f72bc1446
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/utils/Compatibility.java
@@ -0,0 +1,62 @@
+package eu.siacs.conversations.utils;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Build;
+import android.preference.Preference;
+import android.preference.PreferenceCategory;
+import android.preference.PreferenceGroup;
+import android.preference.PreferenceManager;
+import android.support.annotation.BoolRes;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.ui.SettingsActivity;
+import eu.siacs.conversations.ui.SettingsFragment;
+
+public class Compatibility {
+
+ private static final List UNUSED_SETTINGS_POST_TWENTYSIX = Arrays.asList(
+ SettingsActivity.KEEP_FOREGROUND_SERVICE,
+ "led",
+ "notification_ringtone",
+ "notification_headsup",
+ "vibrate_on_notification");
+ private static final List 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 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);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/utils/ConversationsFileObserver.java b/src/main/java/eu/siacs/conversations/utils/ConversationsFileObserver.java
index 33f08d90b..ce11b3424 100644
--- a/src/main/java/eu/siacs/conversations/utils/ConversationsFileObserver.java
+++ b/src/main/java/eu/siacs/conversations/utils/ConversationsFileObserver.java
@@ -2,12 +2,15 @@ package eu.siacs.conversations.utils;
import android.os.FileObserver;
+import android.util.Log;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;
+import eu.siacs.conversations.Config;
+
/**
* Copyright (C) 2012 Bartek Przybylski
* Copyright (C) 2015 ownCloud Inc.
@@ -19,7 +22,7 @@ public abstract class ConversationsFileObserver {
private final String path;
private final List 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);
}
diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml
index 2f4ecbafc..0d0b3d059 100644
--- a/src/main/res/values/strings.xml
+++ b/src/main/res/values/strings.xml
@@ -727,4 +727,15 @@
This group chat has been destroyed
Address book
Unable to save recording
+ Foreground service
+ This notification category is used to display a permanent notification indicating that Conversations is running.
+ Status Information
+ Connectivity Problems
+ This notification category is used to display a notification in case there is a problem connecting to an account.
+ Messages
+ Messages
+ Silent messages
+ This notification group is used to display notifications that should not trigger any sound. For example when being active on another device (Grace Period).
+ Notification Settings
+ Importance, Sound, Vibrate
diff --git a/src/main/res/xml/preferences.xml b/src/main/res/xml/preferences.xml
index e5668bdb9..6c1038b45 100644
--- a/src/main/res/xml/preferences.xml
+++ b/src/main/res/xml/preferences.xml
@@ -1,6 +1,5 @@
-
@@ -10,11 +9,10 @@
+ android:title="@string/huawei_protected_apps">
+ android:targetPackage="com.huawei.systemmanager" />
@@ -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" />
+ android:title="@string/pref_confirm_messages" />
+ android:title="@string/pref_chat_states" />
+ android:title="@string/pref_broadcast_last_activity" />
-
+
+ android:title="@string/pref_notifications" />
+ android:title="@string/pref_notifications_from_strangers" />
+
+
+
+
+
+
+ android:title="@string/pref_headsup_notifications" />
+ android:title="@string/pref_vibrate" />
+ android:title="@string/pref_led" />
+ android:title="@string/pref_sound" />
+ android:value="quiet_hours" />
+ android:title="@string/title_pref_enable_quiet_hours" />
+ android:title="@string/title_pref_quiet_hours_start_time" />
+ android:title="@string/title_pref_quiet_hours_end_time" />
+ android:title="@string/pref_notification_grace_period" />
+ android:title="@string/pref_accept_files" />
+ android:title="@string/pref_picture_compression" />
+ android:title="@string/pref_return_to_previous" />
+ android:title="@string/pref_use_share_location_plugin" />
+ android:title="@string/pref_theme_options" />
+ android:title="@string/pref_use_green_background" />
+ android:title="@string/pref_font_size" />
+ android:title="@string/pref_use_send_button_to_indicate_status" />
+ android:title="@string/pref_quick_action" />
+ android:title="@string/pref_show_dynamic_tags" />
+ android:value="expert" />
+ android:title="@string/pref_blind_trust_before_verification" />
+ android:title="@string/pref_automatically_delete_messages" />
+ android:title="@string/pref_dont_trust_system_cas_title" />
+ android:title="@string/pref_validate_hostname" />
+ android:title="@string/pref_remove_trusted_certificates_title" />
+ android:title="@string/pref_allow_message_correction" />
+ android:title="@string/pref_clean_cache" />
+ android:title="@string/pref_clean_private_storage" />
+ android:title="@string/pref_delete_omemo_identities" />
+ android:title="@string/pref_use_tor" />
+ android:title="@string/pref_show_connection_options" />
+ android:title="@string/pref_start_search" />
+ android:title="@string/pref_enter_is_send" />
+ android:title="@string/pref_display_enter_key" />
+ android:title="@string/pref_scroll_to_bottom" />
+ android:title="@string/pref_manually_change_presence" />
+ android:title="@string/pref_away_when_screen_off" />
+ android:title="@string/pref_dnd_on_silent_mode" />
+ android:title="@string/pref_treat_vibrate_as_silent" />
-
+
+ android:title="@string/pref_autojoin" />
+ android:title="@string/pref_use_indicate_received" />
+ android:title="@string/pref_keep_foreground_service" />
+ android:title="@string/pref_export_logs" />
@@ -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" />
+ android:title="@string/title_activity_about" />