From 676d31f60607656b064e8ada59f314bca75a3c48 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 5 Sep 2018 21:37:05 +0200 Subject: [PATCH] initial work toward api 26+ * introduce notification channels * always use foreground service on 26+ --- build.gradle | 6 +- gradle.properties | 1 - gradle/wrapper/gradle-wrapper.properties | 2 +- src/main/AndroidManifest.xml | 1 + .../services/BarcodeProvider.java | 2 +- .../services/ContactChooserTargetService.java | 2 +- .../conversations/services/EventReceiver.java | 21 +- .../services/ExportLogsService.java | 15 +- .../services/NotificationService.java | 1672 +++++++++-------- .../services/XmppConnectionService.java | 34 +- .../conversations/ui/SettingsActivity.java | 3 +- .../conversations/ui/SettingsFragment.java | 2 + .../siacs/conversations/ui/XmppActivity.java | 8 +- .../conversations/utils/Compatibility.java | 62 + .../utils/ConversationsFileObserver.java | 11 +- src/main/res/values/strings.xml | 11 + src/main/res/xml/preferences.xml | 135 +- 17 files changed, 1074 insertions(+), 914 deletions(-) create mode 100644 src/main/java/eu/siacs/conversations/utils/Compatibility.java 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" />