diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0291bd8aaa..9dbb93c532 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -718,20 +718,6 @@ - - - - - - - - - - - - diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/InMemoryMessageRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/InMemoryMessageRecord.java index 14db5ab3b3..4a49a86879 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/InMemoryMessageRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/InMemoryMessageRecord.java @@ -20,6 +20,7 @@ public class InMemoryMessageRecord extends MessageRecord { private static final int NO_GROUPS_IN_COMMON_ID = -1; private static final int UNIVERSAL_EXPIRE_TIMER_ID = -2; + private static final int FORCE_BUBBLE_ID = -3; private InMemoryMessageRecord(long id, String body, @@ -137,4 +138,13 @@ public class InMemoryMessageRecord extends MessageRecord { return true; } } + + /** + * Useful for create an empty message record when one is needed. + */ + public static final class ForceConversationBubble extends InMemoryMessageRecord { + public ForceConversationBubble(Recipient conversationRecipient, long threadId) { + super(FORCE_BUBBLE_ID, "", conversationRecipient, threadId, 0); + } + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java index aabc6b78f3..26daa1bcb4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java @@ -34,9 +34,7 @@ import org.thoughtcrime.securesms.messages.BackgroundMessageRetriever; import org.thoughtcrime.securesms.messages.IncomingMessageObserver; import org.thoughtcrime.securesms.messages.IncomingMessageProcessor; import org.thoughtcrime.securesms.net.PipeConnectivityListener; -import org.thoughtcrime.securesms.notifications.DefaultMessageNotifier; import org.thoughtcrime.securesms.notifications.MessageNotifier; -import org.thoughtcrime.securesms.notifications.v2.MessageNotifierV2; import org.thoughtcrime.securesms.notifications.OptimizedMessageNotifier; import org.thoughtcrime.securesms.payments.MobileCoinConfig; import org.thoughtcrime.securesms.payments.Payments; diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/AbstractNotificationBuilder.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/AbstractNotificationBuilder.java deleted file mode 100644 index deeaf2059b..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/AbstractNotificationBuilder.java +++ /dev/null @@ -1,103 +0,0 @@ -package org.thoughtcrime.securesms.notifications; - -import android.app.Notification; -import android.content.Context; -import android.graphics.Color; -import android.net.Uri; -import android.text.SpannableStringBuilder; -import android.text.TextUtils; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.app.NotificationCompat; - -import org.signal.core.util.logging.Log; -import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.database.RecipientDatabase; -import org.thoughtcrime.securesms.keyvalue.SignalStore; -import org.thoughtcrime.securesms.preferences.widgets.NotificationPrivacyPreference; -import org.thoughtcrime.securesms.recipients.Recipient; -import org.thoughtcrime.securesms.util.TextSecurePreferences; -import org.thoughtcrime.securesms.util.Util; - -public abstract class AbstractNotificationBuilder extends NotificationCompat.Builder { - - @SuppressWarnings("unused") - private static final String TAG = Log.tag(AbstractNotificationBuilder.class); - - public static final int MAX_DISPLAY_LENGTH = 500; - - protected Context context; - protected NotificationPrivacyPreference privacy; - - public AbstractNotificationBuilder(Context context, NotificationPrivacyPreference privacy) { - super(context); - - this.context = context; - this.privacy = privacy; - - setChannelId(NotificationChannels.getMessagesChannel(context)); - setLed(); - } - - protected CharSequence getStyledMessage(@NonNull Recipient recipient, @Nullable CharSequence message) { - SpannableStringBuilder builder = new SpannableStringBuilder(); - builder.append(Util.getBoldedString(recipient.getDisplayName(context))); - builder.append(": "); - builder.append(message == null ? "" : message); - - return builder; - } - - public void setAlarms(@Nullable Uri ringtone, RecipientDatabase.VibrateState vibrate) { - Uri defaultRingtone = NotificationChannels.supported() ? NotificationChannels.getMessageRingtone(context) : SignalStore.settings().getMessageNotificationSound(); - boolean defaultVibrate = NotificationChannels.supported() ? NotificationChannels.getMessageVibrate(context) : SignalStore.settings().isMessageVibrateEnabled(); - - if (ringtone == null && !TextUtils.isEmpty(defaultRingtone.toString())) setSound(defaultRingtone); - else if (ringtone != null && !ringtone.toString().isEmpty()) setSound(ringtone); - - if (vibrate == RecipientDatabase.VibrateState.ENABLED || - (vibrate == RecipientDatabase.VibrateState.DEFAULT && defaultVibrate)) - { - setDefaults(Notification.DEFAULT_VIBRATE); - } - } - - private void setLed() { - String ledColor = SignalStore.settings().getMessageLedColor(); - String ledBlinkPattern = SignalStore.settings().getMessageLedBlinkPattern(); - String ledBlinkPatternCustom = TextSecurePreferences.getNotificationLedPatternCustom(context); - - if (!ledColor.equals("none")) { - String[] blinkPatternArray = parseBlinkPattern(ledBlinkPattern, ledBlinkPatternCustom); - - setLights(Color.parseColor(ledColor), - Integer.parseInt(blinkPatternArray[0]), - Integer.parseInt(blinkPatternArray[1])); - } - } - - public void setTicker(@NonNull Recipient recipient, @Nullable CharSequence message) { - if (privacy.isDisplayMessage()) { - setTicker(getStyledMessage(recipient, trimToDisplayLength(message))); - } else if (privacy.isDisplayContact()) { - setTicker(getStyledMessage(recipient, context.getString(R.string.AbstractNotificationBuilder_new_message))); - } else { - setTicker(context.getString(R.string.AbstractNotificationBuilder_new_message)); - } - } - - private String[] parseBlinkPattern(String blinkPattern, String blinkPatternCustom) { - if (blinkPattern.equals("custom")) - blinkPattern = blinkPatternCustom; - - return blinkPattern.split(","); - } - - protected @NonNull CharSequence trimToDisplayLength(@Nullable CharSequence text) { - text = text == null ? "" : text; - - return text.length() <= MAX_DISPLAY_LENGTH ? text - : text.subSequence(0, MAX_DISPLAY_LENGTH); - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/AndroidAutoHeardReceiver.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/AndroidAutoHeardReceiver.java deleted file mode 100644 index d8f2465cf2..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/AndroidAutoHeardReceiver.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright (C) 2011 Whisper Systems - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.thoughtcrime.securesms.notifications; - -import android.annotation.SuppressLint; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.os.AsyncTask; - -import org.signal.core.util.logging.Log; -import org.thoughtcrime.securesms.database.DatabaseFactory; -import org.thoughtcrime.securesms.database.MessageDatabase.MarkedMessageInfo; -import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; - -import java.util.LinkedList; -import java.util.List; - -/** - * Marks an Android Auto as read after the driver have listened to it - */ -public class AndroidAutoHeardReceiver extends BroadcastReceiver { - - public static final String TAG = Log.tag(AndroidAutoHeardReceiver.class); - public static final String HEARD_ACTION = "org.thoughtcrime.securesms.notifications.ANDROID_AUTO_HEARD"; - public static final String THREAD_IDS_EXTRA = "car_heard_thread_ids"; - public static final String NOTIFICATION_ID_EXTRA = "car_notification_id"; - - @SuppressLint("StaticFieldLeak") - @Override - public void onReceive(final Context context, Intent intent) - { - if (!HEARD_ACTION.equals(intent.getAction())) - return; - - final long[] threadIds = intent.getLongArrayExtra(THREAD_IDS_EXTRA); - - if (threadIds != null) { - int notificationId = intent.getIntExtra(NOTIFICATION_ID_EXTRA, -1); - NotificationCancellationHelper.cancelLegacy(context, notificationId); - - new AsyncTask() { - @Override - protected Void doInBackground(Void... params) { - List messageIdsCollection = new LinkedList<>(); - - for (long threadId : threadIds) { - Log.i(TAG, "Marking meassage as read: " + threadId); - List messageIds = DatabaseFactory.getThreadDatabase(context).setRead(threadId, true); - - messageIdsCollection.addAll(messageIds); - } - - ApplicationDependencies.getMessageNotifier().updateNotification(context); - MarkReadReceiver.process(context, messageIdsCollection); - - return null; - } - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/AndroidAutoReplyReceiver.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/AndroidAutoReplyReceiver.java deleted file mode 100644 index ca6632d7e0..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/AndroidAutoReplyReceiver.java +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright (C) 2011 Whisper Systems - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.thoughtcrime.securesms.notifications; - -import android.annotation.SuppressLint; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.os.AsyncTask; -import android.os.Bundle; - -import androidx.core.app.RemoteInput; - -import org.signal.core.util.logging.Log; -import org.thoughtcrime.securesms.database.DatabaseFactory; -import org.thoughtcrime.securesms.database.MessageDatabase.MarkedMessageInfo; -import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; -import org.thoughtcrime.securesms.mms.OutgoingMediaMessage; -import org.thoughtcrime.securesms.recipients.Recipient; -import org.thoughtcrime.securesms.sms.MessageSender; -import org.thoughtcrime.securesms.sms.OutgoingTextMessage; - -import java.util.Collections; -import java.util.LinkedList; -import java.util.List; - -/** - * Get the response text from the Android Auto and sends an message as a reply - */ -public class AndroidAutoReplyReceiver extends BroadcastReceiver { - - public static final String TAG = Log.tag(AndroidAutoReplyReceiver.class); - public static final String REPLY_ACTION = "org.thoughtcrime.securesms.notifications.ANDROID_AUTO_REPLY"; - public static final String RECIPIENT_EXTRA = "car_recipient"; - public static final String VOICE_REPLY_KEY = "car_voice_reply_key"; - public static final String THREAD_ID_EXTRA = "car_reply_thread_id"; - - @SuppressLint("StaticFieldLeak") - @Override - public void onReceive(final Context context, Intent intent) - { - if (!REPLY_ACTION.equals(intent.getAction())) return; - - Bundle remoteInput = RemoteInput.getResultsFromIntent(intent); - - if (remoteInput == null) return; - - final long threadId = intent.getLongExtra(THREAD_ID_EXTRA, -1); - final CharSequence responseText = getMessageText(intent); - final Recipient recipient = Recipient.resolved(intent.getParcelableExtra(RECIPIENT_EXTRA)); - - if (responseText != null) { - new AsyncTask() { - @Override - protected Void doInBackground(Void... params) { - - long replyThreadId; - - int subscriptionId = recipient.getDefaultSubscriptionId().or(-1); - long expiresIn = recipient.getExpireMessages() * 1000L; - - if (recipient.resolve().isGroup()) { - Log.w(TAG, "GroupRecipient, Sending media message"); - OutgoingMediaMessage reply = new OutgoingMediaMessage(recipient, - responseText.toString(), - new LinkedList<>(), - System.currentTimeMillis(), - subscriptionId, - expiresIn, - false, - 0, - null, - Collections.emptyList(), - Collections.emptyList(), - Collections.emptyList(), - Collections.emptyList(), - Collections.emptyList()); - replyThreadId = MessageSender.send(context, reply, threadId, false, null); - } else { - Log.w(TAG, "Sending regular message "); - OutgoingTextMessage reply = new OutgoingTextMessage(recipient, responseText.toString(), expiresIn, subscriptionId); - replyThreadId = MessageSender.send(context, reply, threadId, false, null); - } - - List messageIds = DatabaseFactory.getThreadDatabase(context).setRead(replyThreadId, true); - - ApplicationDependencies.getMessageNotifier().updateNotification(context); - MarkReadReceiver.process(context, messageIds); - - return null; - } - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } - } - - private CharSequence getMessageText(Intent intent) { - Bundle remoteInput = RemoteInput.getResultsFromIntent(intent); - if (remoteInput != null) { - return remoteInput.getCharSequence(VOICE_REPLY_KEY); - } - return null; - } - -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java deleted file mode 100644 index b7d847febe..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java +++ /dev/null @@ -1,837 +0,0 @@ -/* - * Copyright (C) 2011 Whisper Systems - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.thoughtcrime.securesms.notifications; - -import android.app.AlarmManager; -import android.app.Notification; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.content.Context; -import android.content.Intent; -import android.database.Cursor; -import android.graphics.BitmapFactory; -import android.media.AudioAttributes; -import android.media.AudioManager; -import android.media.Ringtone; -import android.media.RingtoneManager; -import android.net.Uri; -import android.os.Build; -import android.os.TransactionTooLargeException; -import android.service.notification.StatusBarNotification; -import android.text.SpannableString; -import android.text.SpannableStringBuilder; -import android.text.TextUtils; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.StringRes; -import androidx.core.app.NotificationCompat; -import androidx.core.app.NotificationManagerCompat; - -import com.annimon.stream.Stream; - -import org.signal.core.util.concurrent.SignalExecutors; -import org.signal.core.util.logging.Log; -import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.contactshare.Contact; -import org.thoughtcrime.securesms.contactshare.ContactUtil; -import org.thoughtcrime.securesms.conversation.ConversationIntents; -import org.thoughtcrime.securesms.database.DatabaseFactory; -import org.thoughtcrime.securesms.database.MentionUtil; -import org.thoughtcrime.securesms.database.MmsSmsColumns; -import org.thoughtcrime.securesms.database.MmsSmsDatabase; -import org.thoughtcrime.securesms.database.RecipientDatabase; -import org.thoughtcrime.securesms.database.ThreadBodyUtil; -import org.thoughtcrime.securesms.database.model.MessageRecord; -import org.thoughtcrime.securesms.database.model.MmsMessageRecord; -import org.thoughtcrime.securesms.database.model.ReactionRecord; -import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; -import org.thoughtcrime.securesms.keyvalue.SignalStore; -import org.thoughtcrime.securesms.messages.IncomingMessageObserver; -import org.thoughtcrime.securesms.mms.Slide; -import org.thoughtcrime.securesms.mms.SlideDeck; -import org.thoughtcrime.securesms.preferences.widgets.NotificationPrivacyPreference; -import org.thoughtcrime.securesms.recipients.Recipient; -import org.thoughtcrime.securesms.recipients.RecipientUtil; -import org.thoughtcrime.securesms.service.KeyCachingService; -import org.thoughtcrime.securesms.util.BubbleUtil; -import org.thoughtcrime.securesms.util.MediaUtil; -import org.thoughtcrime.securesms.util.MessageRecordUtil; -import org.thoughtcrime.securesms.util.ServiceUtil; -import org.thoughtcrime.securesms.util.SpanUtil; -import org.thoughtcrime.securesms.util.TextSecurePreferences; -import org.thoughtcrime.securesms.webrtc.CallNotificationBuilder; -import org.whispersystems.signalservice.internal.util.Util; - -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.ListIterator; -import java.util.Set; -import java.util.concurrent.Executor; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; - -import me.leolin.shortcutbadger.ShortcutBadger; - - -/** - * Handles posting system notifications for new messages. - * - * - * @author Moxie Marlinspike - */ -public class DefaultMessageNotifier implements MessageNotifier { - - private static final String TAG = Log.tag(DefaultMessageNotifier.class); - - public static final String EXTRA_REMOTE_REPLY = "extra_remote_reply"; - public static final String NOTIFICATION_GROUP = "messages"; - - private static final String EMOJI_REPLACEMENT_STRING = "__EMOJI__"; - public static final long MIN_AUDIBLE_PERIOD_MILLIS = TimeUnit.SECONDS.toMillis(2); - public static final long DESKTOP_ACTIVITY_PERIOD = TimeUnit.MINUTES.toMillis(1); - - private volatile long visibleThread = -1; - private volatile long lastDesktopActivityTimestamp = -1; - private volatile long lastAudibleNotification = -1; - private final CancelableExecutor executor = new CancelableExecutor(); - - @Override - public void setVisibleThread(long threadId) { - visibleThread = threadId; - } - - @Override - public long getVisibleThread() { - return visibleThread; - } - - @Override - public void clearVisibleThread() { - setVisibleThread(-1); - } - - @Override - public void setLastDesktopActivityTimestamp(long timestamp) { - lastDesktopActivityTimestamp = timestamp; - } - - @Override - public void notifyMessageDeliveryFailed(@NonNull Context context, @NonNull Recipient recipient, long threadId) { - if (visibleThread == threadId) { - sendInThreadNotification(context, recipient); - } else { - Intent intent = ConversationIntents.createBuilder(context, recipient.getId(), threadId) - .withDataUri(Uri.parse("custom://" + System.currentTimeMillis())) - .build(); - FailedNotificationBuilder builder = new FailedNotificationBuilder(context, SignalStore.settings().getMessageNotificationsPrivacy(), intent); - - ((NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE)) - .notify((int)threadId, builder.build()); - } - } - - @Override - public void notifyProofRequired(@NonNull Context context, @NonNull Recipient recipient, long threadId) { - if (visibleThread == threadId) { - sendInThreadNotification(context, recipient); - } else { - Log.w(TAG, "[Proof Required] Not notifying on old notifier."); - } - } - - @Override - public void cancelDelayedNotifications() { - executor.cancel(); - } - - private static boolean isDisplayingSummaryNotification(@NonNull Context context) { - if (Build.VERSION.SDK_INT >= 23) { - try { - NotificationManager notificationManager = ServiceUtil.getNotificationManager(context); - StatusBarNotification[] activeNotifications = notificationManager.getActiveNotifications(); - - for (StatusBarNotification activeNotification : activeNotifications) { - if (activeNotification.getId() == NotificationIds.MESSAGE_SUMMARY) { - return true; - } - } - - return false; - - } catch (Throwable e) { - // XXX Android ROM Bug, see #6043 - Log.w(TAG, e); - return false; - } - } else { - return false; - } - } - - private static void cancelOrphanedNotifications(@NonNull Context context, NotificationState notificationState) { - if (Build.VERSION.SDK_INT >= 23) { - try { - NotificationManager notifications = ServiceUtil.getNotificationManager(context); - StatusBarNotification[] activeNotifications = notifications.getActiveNotifications(); - - for (StatusBarNotification notification : activeNotifications) { - boolean validNotification = false; - - if (notification.getId() != NotificationIds.MESSAGE_SUMMARY && - notification.getId() != KeyCachingService.SERVICE_RUNNING_ID && - notification.getId() != IncomingMessageObserver.FOREGROUND_ID && - notification.getId() != NotificationIds.PENDING_MESSAGES && - !CallNotificationBuilder.isWebRtcNotification(notification.getId())) - { - for (NotificationItem item : notificationState.getNotifications()) { - if (notification.getId() == NotificationIds.getNotificationIdForThread(item.getThreadId())) { - validNotification = true; - break; - } - } - - if (!validNotification) { - NotificationCancellationHelper.cancel(context, notification.getId()); - } - } - } - } catch (Throwable e) { - // XXX Android ROM Bug, see #6043 - Log.w(TAG, e); - } - } - } - - @Override - public void updateNotification(@NonNull Context context) { - if (!SignalStore.settings().isMessageNotificationsEnabled()) { - return; - } - - updateNotification(context, -1, false, 0, BubbleUtil.BubbleState.HIDDEN); - } - - @Override - public void updateNotification(@NonNull Context context, long threadId) - { - if (System.currentTimeMillis() - lastDesktopActivityTimestamp < DESKTOP_ACTIVITY_PERIOD) { - Log.i(TAG, "Scheduling delayed notification..."); - executor.execute(new DelayedNotification(context, threadId)); - } else { - updateNotification(context, threadId, true); - } - } - - @Override - public void updateNotification(@NonNull Context context, long threadId, @NonNull BubbleUtil.BubbleState defaultBubbleState) { - updateNotification(context, threadId, false, 0, defaultBubbleState); - } - - @Override - public void updateNotification(@NonNull Context context, - long threadId, - boolean signal) - { - boolean isVisible = visibleThread == threadId; - Recipient recipient = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadId); - - if (shouldNotify(context, recipient, threadId)) { - if (isVisible) { - sendInThreadNotification(context, recipient); - } else { - updateNotification(context, threadId, signal, 0, BubbleUtil.BubbleState.HIDDEN); - } - } - } - - private boolean shouldNotify(@NonNull Context context, @Nullable Recipient recipient, long threadId) { - if (!SignalStore.settings().isMessageNotificationsEnabled()) { - return false; - } - - if (recipient == null || !recipient.isMuted()) { - return true; - } - - return recipient.isPushV2Group() && - recipient.getMentionSetting() == RecipientDatabase.MentionSetting.ALWAYS_NOTIFY && - DatabaseFactory.getMmsDatabase(context).getUnreadMentionCount(threadId) > 0; - } - - @Override - public void updateNotification(@NonNull Context context, - long targetThread, - boolean signal, - int reminderCount, - @NonNull BubbleUtil.BubbleState defaultBubbleState) - { - if (!SignalStore.settings().isMessageNotificationsEnabled()) { - return; - } - - boolean isReminder = reminderCount > 0; - Cursor telcoCursor = null; - - try { - telcoCursor = DatabaseFactory.getMmsSmsDatabase(context).getUnread(); - - if (telcoCursor == null || telcoCursor.isAfterLast()) { - NotificationCancellationHelper.cancelAllMessageNotifications(context); - updateBadge(context, 0); - clearReminder(context); - return; - } - - NotificationState notificationState = constructNotificationState(context, telcoCursor); - - if (signal && (System.currentTimeMillis() - lastAudibleNotification) < MIN_AUDIBLE_PERIOD_MILLIS) { - signal = false; - } else if (signal) { - lastAudibleNotification = System.currentTimeMillis(); - } - - boolean shouldScheduleReminder = signal; - - if (notificationState.hasMultipleThreads()) { - if (Build.VERSION.SDK_INT >= 23) { - for (long threadId : notificationState.getThreads()) { - if (targetThread < 1 || targetThread == threadId) { - sendSingleThreadNotification(context, - new NotificationState(notificationState.getNotificationsForThread(threadId)), - signal && (threadId == targetThread), - true, - isReminder, - (threadId == targetThread) ? defaultBubbleState : BubbleUtil.BubbleState.HIDDEN); - } - } - } - - sendMultipleThreadNotification(context, notificationState, signal && (Build.VERSION.SDK_INT < 23)); - } else { - long thread = notificationState.getNotifications().isEmpty() ? -1 : notificationState.getNotifications().get(0).getThreadId(); - BubbleUtil.BubbleState bubbleState = thread == targetThread ? defaultBubbleState : BubbleUtil.BubbleState.HIDDEN; - - shouldScheduleReminder = sendSingleThreadNotification(context, notificationState, signal, false, isReminder, bubbleState); - - if (isDisplayingSummaryNotification(context)) { - sendMultipleThreadNotification(context, notificationState, false); - } - } - - cancelOrphanedNotifications(context, notificationState); - updateBadge(context, notificationState.getMessageCount()); - - List smsIds = new LinkedList<>(); - List mmsIds = new LinkedList<>(); - for (NotificationItem item : notificationState.getNotifications()) { - if (item.isMms()) { - mmsIds.add(item.getId()); - } else { - smsIds.add(item.getId()); - } - } - DatabaseFactory.getMmsSmsDatabase(context).setNotifiedTimestamp(System.currentTimeMillis(), smsIds, mmsIds); - - if (shouldScheduleReminder) { - scheduleReminder(context, reminderCount); - } - } finally { - if (telcoCursor != null) telcoCursor.close(); - } - } - - private static boolean sendSingleThreadNotification(@NonNull Context context, - @NonNull NotificationState notificationState, - boolean signal, - boolean bundled, - boolean isReminder, - @NonNull BubbleUtil.BubbleState defaultBubbleState) - { - Log.i(TAG, "sendSingleThreadNotification() signal: " + signal + " bundled: " + bundled); - - if (notificationState.getNotifications().isEmpty()) { - if (!bundled) NotificationCancellationHelper.cancelAllMessageNotifications(context); - Log.i(TAG, "[sendSingleThreadNotification] Empty notification state. Skipping."); - return false; - } - - NotificationPrivacyPreference notificationPrivacy = SignalStore.settings().getMessageNotificationsPrivacy(); - SingleRecipientNotificationBuilder builder = new SingleRecipientNotificationBuilder(context, notificationPrivacy); - List notifications = notificationState.getNotifications(); - Recipient recipient = notifications.get(0).getRecipient(); - boolean shouldAlert = signal && (isReminder || Stream.of(notifications).anyMatch(item -> item.getNotifiedTimestamp() == 0)); - int notificationId; - - if (Build.VERSION.SDK_INT >= 23) { - notificationId = NotificationIds.getNotificationIdForThread(notifications.get(0).getThreadId()); - } else { - notificationId = NotificationIds.MESSAGE_SUMMARY; - } - - builder.setThread(notifications.get(0).getRecipient()); - builder.setMessageCount(notificationState.getMessageCount()); - builder.setPrimaryMessageBody(recipient, notifications.get(0).getIndividualRecipient(), - notifications.get(0).getText(), notifications.get(0).getSlideDeck()); - builder.setContentIntent(notifications.get(0).getPendingIntent(context)); - builder.setDeleteIntent(notificationState.getDeleteIntent(context)); - builder.setOnlyAlertOnce(!shouldAlert); - builder.setSortKey(String.valueOf(Long.MAX_VALUE - notifications.get(0).getTimestamp())); - builder.setDefaultBubbleState(defaultBubbleState); - - long timestamp = notifications.get(0).getTimestamp(); - if (timestamp != 0) builder.setWhen(timestamp); - - boolean isSingleNotificationContactJoined = notifications.size() == 1 && notifications.get(0).isJoin(); - - if (notificationPrivacy.isDisplayMessage() && - !KeyCachingService.isLocked(context) && - RecipientUtil.isMessageRequestAccepted(context, recipient.resolve())) - { - ReplyMethod replyMethod = ReplyMethod.forRecipient(context, recipient); - - builder.addActions(notificationState.getMarkAsReadIntent(context, notificationId), - notificationState.getQuickReplyIntent(context, notifications.get(0).getRecipient()), - notificationState.getRemoteReplyIntent(context, notifications.get(0).getRecipient(), replyMethod), - replyMethod, - !isSingleNotificationContactJoined && notificationState.canReply()); - - builder.addAndroidAutoAction(notificationState.getAndroidAutoReplyIntent(context, notifications.get(0).getRecipient()), - notificationState.getAndroidAutoHeardIntent(context, notificationId), notifications.get(0).getTimestamp()); - } - - if (!KeyCachingService.isLocked(context) && isSingleNotificationContactJoined) { - builder.addTurnOffTheseNotificationsAction(notificationState.getTurnOffTheseNotificationsIntent(context)); - } - - ListIterator iterator = notifications.listIterator(notifications.size()); - - while(iterator.hasPrevious()) { - NotificationItem item = iterator.previous(); - builder.addMessageBody(item.getRecipient(), item.getIndividualRecipient(), item.getText(), item.getTimestamp(), item.getSlideDeck()); - } - - if (signal) { - builder.setAlarms(notificationState.getRingtone(context), notificationState.getVibrate()); - builder.setTicker(notifications.get(0).getIndividualRecipient(), - notifications.get(0).getText()); - } - - if (Build.VERSION.SDK_INT >= 23) { - builder.setGroup(NOTIFICATION_GROUP); - builder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN); - } - - Notification notification = builder.build(); - try { - NotificationManagerCompat.from(context).notify(notificationId, notification); - Log.i(TAG, "Posted notification."); - } catch (SecurityException e) { - Uri defaultValue = SignalStore.settings().getMessageNotificationSound(); - if (!defaultValue.equals(notificationState.getRingtone(context))) { - Log.e(TAG, "Security exception when posting notification with custom ringtone", e); - clearNotificationRingtone(context, notifications.get(0).getRecipient()); - } else { - throw e; - } - } - - return shouldAlert; - } - - private static void sendMultipleThreadNotification(@NonNull Context context, - @NonNull NotificationState notificationState, - boolean signal) - { - Log.i(TAG, "sendMultiThreadNotification() signal: " + signal); - - if (notificationState.getNotifications().isEmpty()) { - Log.i(TAG, "[sendMultiThreadNotification] Empty notification state. Skipping."); - return; - } - - NotificationPrivacyPreference notificationPrivacy = SignalStore.settings().getMessageNotificationsPrivacy(); - MultipleRecipientNotificationBuilder builder = new MultipleRecipientNotificationBuilder(context, notificationPrivacy); - List notifications = notificationState.getNotifications(); - boolean shouldAlert = signal && Stream.of(notifications).anyMatch(item -> item.getNotifiedTimestamp() == 0); - - builder.setMessageCount(notificationState.getMessageCount(), notificationState.getThreadCount()); - builder.setMostRecentSender(notifications.get(0).getIndividualRecipient()); - builder.setDeleteIntent(notificationState.getDeleteIntent(context)); - builder.setOnlyAlertOnce(!shouldAlert); - - if (Build.VERSION.SDK_INT >= 23) { - builder.setGroup(NOTIFICATION_GROUP); - builder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN); - } - - long timestamp = notifications.get(0).getTimestamp(); - if (timestamp != 0) builder.setWhen(timestamp); - - if (notificationPrivacy.isDisplayMessage()) { - builder.addActions(notificationState.getMarkAsReadIntent(context, NotificationIds.MESSAGE_SUMMARY)); - } - - ListIterator iterator = notifications.listIterator(notifications.size()); - - while(iterator.hasPrevious()) { - NotificationItem item = iterator.previous(); - builder.addMessageBody(item.getIndividualRecipient(), item.getText()); - } - - if (signal) { - builder.setAlarms(notificationState.getRingtone(context), notificationState.getVibrate()); - builder.setTicker(notifications.get(0).getIndividualRecipient(), - notifications.get(0).getText()); - } - - Notification notification = builder.build(); - - try { - NotificationManagerCompat.from(context).notify(NotificationIds.MESSAGE_SUMMARY, builder.build()); - Log.i(TAG, "Posted notification. " + notification.toString()); - } catch (SecurityException securityException) { - Uri defaultValue = SignalStore.settings().getMessageNotificationSound(); - if (!defaultValue.equals(notificationState.getRingtone(context))) { - Log.e(TAG, "Security exception when posting notification with custom ringtone", securityException); - clearNotificationRingtone(context, notifications.get(0).getRecipient()); - } else { - throw securityException; - } - } catch (RuntimeException runtimeException) { - Throwable cause = runtimeException.getCause(); - if (cause instanceof TransactionTooLargeException) { - Log.e(TAG, "Transaction too large", runtimeException); - } else { - throw runtimeException; - } - } - } - - private static void sendInThreadNotification(Context context, Recipient recipient) { - if (!SignalStore.settings().isMessageNotificationsInChatSoundsEnabled() || - ServiceUtil.getAudioManager(context).getRingerMode() != AudioManager.RINGER_MODE_NORMAL) - { - return; - } - - Uri uri = null; - if (recipient != null) { - uri = NotificationChannels.supported() ? NotificationChannels.getMessageRingtone(context, recipient) : recipient.getMessageRingtone(); - } - - if (uri == null) { - uri = NotificationChannels.supported() ? NotificationChannels.getMessageRingtone(context) : SignalStore.settings().getMessageNotificationSound(); - } - - if (uri.toString().isEmpty()) { - Log.d(TAG, "ringtone uri is empty"); - return; - } - - Ringtone ringtone = RingtoneManager.getRingtone(context, uri); - - if (ringtone == null) { - Log.w(TAG, "ringtone is null"); - return; - } - - if (Build.VERSION.SDK_INT >= 21) { - ringtone.setAudioAttributes(new AudioAttributes.Builder().setContentType(AudioAttributes.CONTENT_TYPE_UNKNOWN) - .setUsage(AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT) - .build()); - } else { - ringtone.setStreamType(AudioManager.STREAM_NOTIFICATION); - } - - ringtone.play(); - } - - private static NotificationState constructNotificationState(@NonNull Context context, - @NonNull Cursor cursor) - { - NotificationState notificationState = new NotificationState(); - MmsSmsDatabase.Reader reader = DatabaseFactory.getMmsSmsDatabase(context).readerFor(cursor); - - MessageRecord record; - - while ((record = reader.getNext()) != null) { - long id = record.getId(); - boolean mms = record.isMms() || record.isMmsNotification(); - Recipient recipient = record.getIndividualRecipient().resolve(); - Recipient conversationRecipient = record.getRecipient().resolve(); - long threadId = record.getThreadId(); - CharSequence body = MentionUtil.updateBodyWithDisplayNames(context, record); - Recipient threadRecipients = null; - SlideDeck slideDeck = null; - long timestamp = record.getTimestamp(); - long receivedTimestamp = record.getDateReceived(); - boolean isUnreadMessage = cursor.getInt(cursor.getColumnIndexOrThrow(MmsSmsColumns.READ)) == 0; - boolean hasUnreadReactions = cursor.getInt(cursor.getColumnIndexOrThrow(MmsSmsColumns.REACTIONS_UNREAD)) == 1; - long lastReactionRead = cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsColumns.REACTIONS_LAST_SEEN)); - long notifiedTimestamp = record.getNotifiedTimestamp(); - - if (threadId != -1) { - threadRecipients = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadId); - } - - if (isUnreadMessage) { - boolean canReply = false; - - if (!RecipientUtil.isMessageRequestAccepted(context, threadId)) { - body = SpanUtil.italic(context.getString(R.string.SingleRecipientNotificationBuilder_message_request)); - } else if (KeyCachingService.isLocked(context)) { - body = SpanUtil.italic(context.getString(R.string.MessageNotifier_locked_message)); - } else if (record.isMms() && !((MmsMessageRecord) record).getSharedContacts().isEmpty()) { - Contact contact = ((MmsMessageRecord) record).getSharedContacts().get(0); - body = ContactUtil.getStringSummary(context, contact); - } else if (record.isMms() && ((MmsMessageRecord) record).isViewOnce()) { - body = SpanUtil.italic(context.getString(getViewOnceDescription((MmsMessageRecord) record))); - } else if (record.isRemoteDelete()) { - body = SpanUtil.italic(context.getString(R.string.MessageNotifier_this_message_was_deleted));; - } else if (record.isMms() && !record.isMmsNotification() && !((MmsMessageRecord) record).getSlideDeck().getSlides().isEmpty()) { - body = ThreadBodyUtil.getFormattedBodyFor(context, record); - slideDeck = ((MmsMessageRecord) record).getSlideDeck(); - canReply = true; - } else if (record.isGroupCall()) { - body = new SpannableString(MessageRecord.getGroupCallUpdateDescription(context, record.getBody(), false).getString()); - canReply = false; - } else { - canReply = true; - } - - boolean includeMessage = true; - if (threadRecipients != null && threadRecipients.isMuted()) { - boolean mentionsOverrideMute = threadRecipients.getMentionSetting() == RecipientDatabase.MentionSetting.ALWAYS_NOTIFY; - - includeMessage = mentionsOverrideMute && record.hasSelfMention(); - } - - if (threadRecipients == null || includeMessage) { - notificationState.addNotification(new NotificationItem(id, mms, recipient, conversationRecipient, threadRecipients, threadId, body, timestamp, receivedTimestamp, slideDeck, false, record.isJoined(), canReply, notifiedTimestamp)); - } - } - - if (hasUnreadReactions) { - CharSequence originalBody = body; - for (ReactionRecord reaction : record.getReactions()) { - Recipient reactionSender = Recipient.resolved(reaction.getAuthor()); - if (reactionSender.equals(Recipient.self()) || !record.isOutgoing() || reaction.getDateReceived() <= lastReactionRead) { - continue; - } - - if (KeyCachingService.isLocked(context)) { - body = SpanUtil.italic(context.getString(R.string.MessageNotifier_locked_message)); - } else { - String text = SpanUtil.italic(getReactionMessageBody(context, record, originalBody)).toString(); - String[] parts = text.split(EMOJI_REPLACEMENT_STRING); - - SpannableStringBuilder builder = new SpannableStringBuilder(); - for (int i = 0; i < parts.length; i++) { - builder.append(SpanUtil.italic(parts[i])); - - if (i != parts.length -1) { - builder.append(reaction.getEmoji()); - } - } - - if (text.endsWith(EMOJI_REPLACEMENT_STRING)) { - builder.append(reaction.getEmoji()); - } - - body = builder; - } - - if (threadRecipients == null || !threadRecipients.isMuted()) { - notificationState.addNotification(new NotificationItem(id, mms, reactionSender, conversationRecipient, threadRecipients, threadId, body, reaction.getDateReceived(), receivedTimestamp, null, true, record.isJoined(), false, 0)); - } - } - } - } - - reader.close(); - return notificationState; - } - - private static CharSequence getReactionMessageBody(@NonNull Context context, @NonNull MessageRecord record, @NonNull CharSequence body) { - boolean bodyIsEmpty = TextUtils.isEmpty(body); - - if (MessageRecordUtil.hasSharedContact(record)) { - Contact contact = ((MmsMessageRecord) record).getSharedContacts().get(0); - CharSequence summary = ContactUtil.getStringSummary(context, contact); - - return context.getString(R.string.MessageNotifier_reacted_s_to_s, EMOJI_REPLACEMENT_STRING, summary); - } else if (MessageRecordUtil.hasSticker(record)) { - return context.getString(R.string.MessageNotifier_reacted_s_to_your_sticker, EMOJI_REPLACEMENT_STRING); - } else if (record.isMms() && record.isViewOnce()){ - return context.getString(R.string.MessageNotifier_reacted_s_to_your_view_once_media, EMOJI_REPLACEMENT_STRING); - } else if (!bodyIsEmpty) { - return context.getString(R.string.MessageNotifier_reacted_s_to_s, EMOJI_REPLACEMENT_STRING, body); - } else if (MessageRecordUtil.isMediaMessage(record) && MediaUtil.isVideoType(getMessageContentType((MmsMessageRecord) record))) { - return context.getString(R.string.MessageNotifier_reacted_s_to_your_video, EMOJI_REPLACEMENT_STRING); - } else if (MessageRecordUtil.isMediaMessage(record) && MediaUtil.isImageType(getMessageContentType((MmsMessageRecord) record))) { - return context.getString(R.string.MessageNotifier_reacted_s_to_your_image, EMOJI_REPLACEMENT_STRING); - } else if (MessageRecordUtil.isMediaMessage(record) && MediaUtil.isAudioType(getMessageContentType((MmsMessageRecord) record))) { - return context.getString(R.string.MessageNotifier_reacted_s_to_your_audio, EMOJI_REPLACEMENT_STRING); - } else if (MessageRecordUtil.isMediaMessage(record)) { - return context.getString(R.string.MessageNotifier_reacted_s_to_your_file, EMOJI_REPLACEMENT_STRING); - } else { - return context.getString(R.string.MessageNotifier_reacted_s_to_s, EMOJI_REPLACEMENT_STRING, body); - } - } - - private static @StringRes int getViewOnceDescription(@NonNull MmsMessageRecord messageRecord) { - final String contentType = getMessageContentType(messageRecord); - - if (MediaUtil.isImageType(contentType)) { - return R.string.MessageNotifier_view_once_photo; - } - return R.string.MessageNotifier_view_once_video; - } - - private static String getMessageContentType(@NonNull MmsMessageRecord messageRecord) { - Slide thumbnailSlide = messageRecord.getSlideDeck().getThumbnailSlide(); - if (thumbnailSlide == null) { - String slideContentType = messageRecord.getSlideDeck().getFirstSlideContentType(); - if (slideContentType != null) { - return slideContentType; - } - Log.w(TAG, "Could not distinguish view-once content type from message record, defaulting to JPEG"); - return MediaUtil.IMAGE_JPEG; - } - return thumbnailSlide.getContentType(); - } - - private static void updateBadge(Context context, int count) { - try { - if (count == 0) ShortcutBadger.removeCount(context); - else ShortcutBadger.applyCount(context, count); - } catch (Throwable t) { - // NOTE :: I don't totally trust this thing, so I'm catching - // everything. - Log.w(TAG, t); - } - } - - private static void scheduleReminder(Context context, int count) { - if (count >= SignalStore.settings().getMessageNotificationsRepeatAlerts()) { - return; - } - - AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); - Intent alarmIntent = new Intent(context, ReminderReceiver.class); - alarmIntent.putExtra("reminder_count", count); - - PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, alarmIntent, PendingIntent.FLAG_CANCEL_CURRENT); - long timeout = TimeUnit.MINUTES.toMillis(2); - - alarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + timeout, pendingIntent); - } - - private static void clearNotificationRingtone(@NonNull Context context, @NonNull Recipient recipient) { - SignalExecutors.BOUNDED.execute(() -> { - DatabaseFactory.getRecipientDatabase(context).setMessageRingtone(recipient.getId(), null); - NotificationChannels.updateMessageRingtone(context, recipient, null); - }); - } - - @Override - public void clearReminder(@NonNull Context context) { - Intent alarmIntent = new Intent(context, ReminderReceiver.class); - PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, alarmIntent, PendingIntent.FLAG_CANCEL_CURRENT); - AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); - - alarmManager.cancel(pendingIntent); - } - - @Override - public void addStickyThread(long threadId, long earliestTimestamp) {} - - @Override - public void removeStickyThread(long threadId) {} - - private static class DelayedNotification implements Runnable { - - private static final long DELAY = TimeUnit.SECONDS.toMillis(5); - - private final AtomicBoolean canceled = new AtomicBoolean(false); - - private final Context context; - private final long threadId; - private final long delayUntil; - - private DelayedNotification(Context context, long threadId) { - this.context = context; - this.threadId = threadId; - this.delayUntil = System.currentTimeMillis() + DELAY; - } - - @Override - public void run() { - long delayMillis = delayUntil - System.currentTimeMillis(); - Log.i(TAG, "Waiting to notify: " + delayMillis); - - if (delayMillis > 0) { - Util.sleep(delayMillis); - } - - if (!canceled.get()) { - Log.i(TAG, "Not canceled, notifying..."); - ApplicationDependencies.getMessageNotifier().updateNotification(context, threadId, true); - ApplicationDependencies.getMessageNotifier().cancelDelayedNotifications(); - } else { - Log.w(TAG, "Canceled, not notifying..."); - } - } - - public void cancel() { - canceled.set(true); - } - } - - private static class CancelableExecutor { - - private final Executor executor = Executors.newSingleThreadExecutor(); - private final Set tasks = new HashSet<>(); - - public void execute(final DelayedNotification runnable) { - synchronized (tasks) { - tasks.add(runnable); - } - - Runnable wrapper = () -> { - runnable.run(); - - synchronized (tasks) { - tasks.remove(runnable); - } - }; - - executor.execute(wrapper); - } - - public void cancel() { - synchronized (tasks) { - for (DelayedNotification task : tasks) { - task.cancel(); - } - } - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/FailedNotificationBuilder.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/FailedNotificationBuilder.java deleted file mode 100644 index 084785932b..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/FailedNotificationBuilder.java +++ /dev/null @@ -1,31 +0,0 @@ -package org.thoughtcrime.securesms.notifications; - -import android.app.PendingIntent; -import android.content.Context; -import android.content.Intent; -import android.graphics.BitmapFactory; - -import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.database.RecipientDatabase; -import org.thoughtcrime.securesms.preferences.widgets.NotificationPrivacyPreference; - -public class FailedNotificationBuilder extends AbstractNotificationBuilder { - - public FailedNotificationBuilder(Context context, NotificationPrivacyPreference privacy, Intent intent) { - super(context, privacy); - - setSmallIcon(R.drawable.ic_notification); - setLargeIcon(BitmapFactory.decodeResource(context.getResources(), - R.drawable.ic_action_warning_red)); - setContentTitle(context.getString(R.string.MessageNotifier_message_delivery_failed)); - setContentText(context.getString(R.string.MessageNotifier_failed_to_deliver_message)); - setTicker(context.getString(R.string.MessageNotifier_error_delivering_message)); - setContentIntent(PendingIntent.getActivity(context, 0, intent, 0)); - setAutoCancel(true); - setAlarms(null, RecipientDatabase.VibrateState.DEFAULT); - setChannelId(NotificationChannels.FAILURES); - } - - - -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/MultipleRecipientNotificationBuilder.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/MultipleRecipientNotificationBuilder.java deleted file mode 100644 index f9ebfa8304..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/MultipleRecipientNotificationBuilder.java +++ /dev/null @@ -1,94 +0,0 @@ -package org.thoughtcrime.securesms.notifications; - -import android.app.Notification; -import android.app.PendingIntent; -import android.content.Context; -import android.content.Intent; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.app.NotificationCompat; - -import org.thoughtcrime.securesms.MainActivity; -import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.preferences.widgets.NotificationPrivacyPreference; -import org.thoughtcrime.securesms.recipients.Recipient; -import org.thoughtcrime.securesms.util.TextSecurePreferences; -import org.thoughtcrime.securesms.util.Util; - -import java.util.LinkedList; -import java.util.List; - -public class MultipleRecipientNotificationBuilder extends AbstractNotificationBuilder { - - private final List messageBodies = new LinkedList<>(); - - public MultipleRecipientNotificationBuilder(Context context, NotificationPrivacyPreference privacy) { - super(context, privacy); - - setColor(context.getResources().getColor(R.color.core_ultramarine)); - setSmallIcon(R.drawable.ic_notification); - setContentTitle(context.getString(R.string.app_name)); - // TODO [greyson] Navigation - setContentIntent(PendingIntent.getActivity(context, 0, MainActivity.clearTop(context), 0)); - setCategory(NotificationCompat.CATEGORY_MESSAGE); - setGroupSummary(true); - - if (!NotificationChannels.supported()) { - setPriority(TextSecurePreferences.getNotificationPriority(context)); - } - } - - public void setMessageCount(int messageCount, int threadCount) { - setSubText(context.getString(R.string.MessageNotifier_d_new_messages_in_d_conversations, - messageCount, threadCount)); - setContentInfo(String.valueOf(messageCount)); - setNumber(messageCount); - } - - public void setMostRecentSender(Recipient recipient) { - if (privacy.isDisplayContact()) { - setContentText(context.getString(R.string.MessageNotifier_most_recent_from_s, - recipient.getDisplayName(context))); - } - - if (recipient.getNotificationChannel() != null) { - setChannelId(recipient.getNotificationChannel()); - } - } - - public void addActions(PendingIntent markAsReadIntent) { - NotificationCompat.Action markAllAsReadAction = new NotificationCompat.Action(R.drawable.check, - context.getString(R.string.MessageNotifier_mark_all_as_read), - markAsReadIntent); - addAction(markAllAsReadAction); - extend(new NotificationCompat.WearableExtender().addAction(markAllAsReadAction)); - } - - public void addMessageBody(@NonNull Recipient sender, @Nullable CharSequence body) { - if (privacy.isDisplayMessage()) { - messageBodies.add(getStyledMessage(sender, body)); - } else if (privacy.isDisplayContact()) { - messageBodies.add(Util.getBoldedString(sender.getDisplayName(context))); - } - - if (privacy.isDisplayContact() && sender.getContactUri() != null) { - addPerson(sender.getContactUri().toString()); - } - } - - @Override - public Notification build() { - if (privacy.isDisplayMessage() || privacy.isDisplayContact()) { - NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle(); - - for (CharSequence body : messageBodies) { - style.addLine(trimToDisplayLength(body)); - } - - setStyle(style); - } - - return super.build(); - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationCancellationHelper.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationCancellationHelper.java index 842fe59ac6..904bffb9a2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationCancellationHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationCancellationHelper.java @@ -14,6 +14,7 @@ import com.annimon.stream.Stream; import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; +import org.thoughtcrime.securesms.notifications.v2.MessageNotifierV2; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.util.BubbleUtil; import org.thoughtcrime.securesms.util.ConversationUtil; @@ -44,7 +45,7 @@ public final class NotificationCancellationHelper { /** * Cancels all Message-Based notifications. Specifically, this is any notification that is not the - * summary notification assigned to the {@link DefaultMessageNotifier#NOTIFICATION_GROUP} group. + * summary notification assigned to the {@link MessageNotifierV2#NOTIFICATION_GROUP} group. * * We utilize our wrapped cancellation methods and a counter to make sure that we do not lose * bubble notifications that do not have unread messages in them. @@ -110,7 +111,7 @@ public final class NotificationCancellationHelper { @RequiresApi(23) private static boolean isSingleThreadNotification(@NonNull StatusBarNotification statusBarNotification) { return statusBarNotification.getId() != NotificationIds.MESSAGE_SUMMARY && - Objects.equals(statusBarNotification.getNotification().getGroup(), DefaultMessageNotifier.NOTIFICATION_GROUP); + Objects.equals(statusBarNotification.getNotification().getGroup(), MessageNotifierV2.NOTIFICATION_GROUP); } /** diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationItem.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationItem.java deleted file mode 100644 index 66b679d54f..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationItem.java +++ /dev/null @@ -1,131 +0,0 @@ -package org.thoughtcrime.securesms.notifications; - -import android.app.PendingIntent; -import android.content.Context; -import android.content.Intent; -import android.net.Uri; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.app.TaskStackBuilder; - -import org.thoughtcrime.securesms.conversation.ConversationIntents; -import org.thoughtcrime.securesms.database.DatabaseFactory; -import org.thoughtcrime.securesms.mms.SlideDeck; -import org.thoughtcrime.securesms.recipients.Recipient; - -public class NotificationItem { - - private final long id; - private final boolean mms; - @NonNull private final Recipient conversationRecipient; - @NonNull private final Recipient individualRecipient; - @Nullable private final Recipient threadRecipient; - private final long threadId; - @Nullable private final CharSequence text; - private final long timestamp; - private final long messageReceivedTimestamp; - @Nullable private final SlideDeck slideDeck; - private final boolean jumpToMessage; - private final boolean isJoin; - private final boolean canReply; - private final long notifiedTimestamp; - - public NotificationItem(long id, - boolean mms, - @NonNull Recipient individualRecipient, - @NonNull Recipient conversationRecipient, - @Nullable Recipient threadRecipient, - long threadId, - @Nullable CharSequence text, - long timestamp, - long messageReceivedTimestamp, - @Nullable SlideDeck slideDeck, - boolean jumpToMessage, - boolean isJoin, - boolean canReply, - long notifiedTimestamp) - { - this.id = id; - this.mms = mms; - this.individualRecipient = individualRecipient; - this.conversationRecipient = conversationRecipient; - this.threadRecipient = threadRecipient; - this.text = text; - this.threadId = threadId; - this.timestamp = timestamp; - this.messageReceivedTimestamp = messageReceivedTimestamp; - this.slideDeck = slideDeck; - this.jumpToMessage = jumpToMessage; - this.isJoin = isJoin; - this.canReply = canReply; - this.notifiedTimestamp = notifiedTimestamp; - } - - public @NonNull Recipient getRecipient() { - return threadRecipient == null ? conversationRecipient : threadRecipient; - } - - public @NonNull Recipient getIndividualRecipient() { - return individualRecipient; - } - - public @Nullable CharSequence getText() { - return text; - } - - public long getTimestamp() { - return timestamp; - } - - public long getThreadId() { - return threadId; - } - - public @Nullable SlideDeck getSlideDeck() { - return slideDeck; - } - - public PendingIntent getPendingIntent(Context context) { - Recipient recipient = threadRecipient != null ? threadRecipient : conversationRecipient; - int startingPosition = jumpToMessage ? getStartingPosition(context, threadId, messageReceivedTimestamp) : -1; - - Intent intent = ConversationIntents.createBuilder(context, recipient.getId(), threadId) - .withStartingPosition(startingPosition) - .build(); - - makeIntentUniqueToPreventMerging(intent); - - return TaskStackBuilder.create(context) - .addNextIntentWithParentStack(intent) - .getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT); - } - - public long getId() { - return id; - } - - public boolean isMms() { - return mms; - } - - public boolean isJoin() { - return isJoin; - } - - private static int getStartingPosition(@NonNull Context context, long threadId, long receivedTimestampMs) { - return DatabaseFactory.getMmsSmsDatabase(context).getMessagePositionInConversation(threadId, receivedTimestampMs); - } - - private static void makeIntentUniqueToPreventMerging(@NonNull Intent intent) { - intent.setData((Uri.parse("custom://"+System.currentTimeMillis()))); - } - - public boolean canReply() { - return canReply; - } - - public long getNotifiedTimestamp() { - return notifiedTimestamp; - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationState.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationState.java deleted file mode 100644 index 2f905d45d9..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationState.java +++ /dev/null @@ -1,213 +0,0 @@ -package org.thoughtcrime.securesms.notifications; - -import android.app.PendingIntent; -import android.content.Context; -import android.content.Intent; -import android.net.Uri; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import org.signal.core.util.logging.Log; -import org.thoughtcrime.securesms.contacts.TurnOffContactJoinedNotificationsActivity; -import org.thoughtcrime.securesms.conversation.ConversationIntents; -import org.thoughtcrime.securesms.database.RecipientDatabase.VibrateState; -import org.thoughtcrime.securesms.recipients.Recipient; - -import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; -import java.util.LinkedHashSet; -import java.util.LinkedList; -import java.util.List; - -public class NotificationState { - - private static final String TAG = Log.tag(NotificationState.class); - - private final Comparator notificationItemComparator = (a, b) -> -Long.compare(a.getTimestamp(), b.getTimestamp()); - private final List notifications = new LinkedList<>(); - private final LinkedHashSet threads = new LinkedHashSet<>(); - - public NotificationState() {} - - public NotificationState(@NonNull List items) { - for (NotificationItem item : items) { - addNotification(item); - } - } - - public void addNotification(NotificationItem item) { - notifications.add(item); - Collections.sort(notifications, notificationItemComparator); - - threads.remove(item.getThreadId()); - threads.add(item.getThreadId()); - } - - public @Nullable Uri getRingtone(@NonNull Context context) { - if (!notifications.isEmpty()) { - Recipient recipient = notifications.get(0).getRecipient(); - - if (recipient != null) { - return NotificationChannels.supported() ? NotificationChannels.getMessageRingtone(context, recipient) - : recipient.resolve().getMessageRingtone(); - } - } - - return null; - } - - public VibrateState getVibrate() { - if (!notifications.isEmpty()) { - Recipient recipient = notifications.get(0).getRecipient(); - - if (recipient != null) { - return recipient.resolve().getMessageVibrate(); - } - } - - return VibrateState.DEFAULT; - } - - public boolean hasMultipleThreads() { - return threads.size() > 1; - } - - public Collection getThreads() { - return threads; - } - - public int getThreadCount() { - return threads.size(); - } - - public int getMessageCount() { - return notifications.size(); - } - - public List getNotifications() { - return notifications; - } - - public List getNotificationsForThread(long threadId) { - List list = new LinkedList<>(); - - for (NotificationItem item : notifications) { - if (item.getThreadId() == threadId) list.add(item); - } - - Collections.sort(list, notificationItemComparator); - return list; - } - - public PendingIntent getTurnOffTheseNotificationsIntent(Context context) { - long threadId = threads.iterator().next(); - - return PendingIntent.getActivity(context, - 0, - TurnOffContactJoinedNotificationsActivity.newIntent(context, threadId), - PendingIntent.FLAG_UPDATE_CURRENT); - } - - public PendingIntent getMarkAsReadIntent(Context context, int notificationId) { - long[] threadArray = new long[threads.size()]; - int index = 0; - StringBuilder threadString = new StringBuilder(); - - for (long thread : threads) { - threadString.append(thread).append(" "); - threadArray[index++] = thread; - } - - Log.i(TAG, "Added threads: " + threadString.toString()); - - Intent intent = new Intent(MarkReadReceiver.CLEAR_ACTION); - intent.setClass(context, MarkReadReceiver.class); - intent.setData((Uri.parse("custom://"+System.currentTimeMillis()))); - intent.putExtra(MarkReadReceiver.THREAD_IDS_EXTRA, threadArray); - intent.putExtra(MarkReadReceiver.NOTIFICATION_ID_EXTRA, notificationId); - - return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); - } - - public PendingIntent getRemoteReplyIntent(Context context, Recipient recipient, ReplyMethod replyMethod) { - if (threads.size() != 1) throw new AssertionError("We only support replies to single thread notifications!"); - - Intent intent = new Intent(RemoteReplyReceiver.REPLY_ACTION); - intent.setClass(context, RemoteReplyReceiver.class); - intent.setData((Uri.parse("custom://"+System.currentTimeMillis()))); - intent.putExtra(RemoteReplyReceiver.RECIPIENT_EXTRA, recipient.getId()); - intent.putExtra(RemoteReplyReceiver.REPLY_METHOD, replyMethod); - intent.setPackage(context.getPackageName()); - - return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); - } - - public PendingIntent getAndroidAutoReplyIntent(Context context, Recipient recipient) { - if (threads.size() != 1) throw new AssertionError("We only support replies to single thread notifications!"); - - Intent intent = new Intent(AndroidAutoReplyReceiver.REPLY_ACTION); - intent.addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES); - intent.setClass(context, AndroidAutoReplyReceiver.class); - intent.setData((Uri.parse("custom://"+System.currentTimeMillis()))); - intent.putExtra(AndroidAutoReplyReceiver.RECIPIENT_EXTRA, recipient.getId()); - intent.putExtra(AndroidAutoReplyReceiver.THREAD_ID_EXTRA, (long)threads.toArray()[0]); - intent.setPackage(context.getPackageName()); - - return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); - } - - public PendingIntent getAndroidAutoHeardIntent(Context context, int notificationId) { - long[] threadArray = new long[threads.size()]; - int index = 0; - for (long thread : threads) { - Log.i(TAG, "getAndroidAutoHeardIntent Added thread: " + thread); - threadArray[index++] = thread; - } - - Intent intent = new Intent(AndroidAutoHeardReceiver.HEARD_ACTION); - intent.addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES); - intent.setClass(context, AndroidAutoHeardReceiver.class); - intent.setData((Uri.parse("custom://"+System.currentTimeMillis()))); - intent.putExtra(AndroidAutoHeardReceiver.THREAD_IDS_EXTRA, threadArray); - intent.putExtra(AndroidAutoHeardReceiver.NOTIFICATION_ID_EXTRA, notificationId); - intent.setPackage(context.getPackageName()); - - return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); - } - - public PendingIntent getQuickReplyIntent(Context context, Recipient recipient) { - if (threads.size() != 1) throw new AssertionError("We only support replies to single thread notifications! " + threads.size()); - - Intent intent = ConversationIntents.createPopUpBuilder(context, recipient.getId(), (long) threads.toArray()[0]) - .withDataUri(Uri.parse("custom://"+System.currentTimeMillis())) - .build(); - - return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); - } - - public PendingIntent getDeleteIntent(Context context) { - int index = 0; - long[] ids = new long[notifications.size()]; - boolean[] mms = new boolean[ids.length]; - - for (NotificationItem notificationItem : notifications) { - ids[index] = notificationItem.getId(); - mms[index++] = notificationItem.isMms(); - } - - Intent intent = new Intent(context, DeleteNotificationReceiver.class); - intent.setAction(DeleteNotificationReceiver.DELETE_NOTIFICATION_ACTION); - intent.putExtra(DeleteNotificationReceiver.EXTRA_IDS, ids); - intent.putExtra(DeleteNotificationReceiver.EXTRA_MMS, mms); - intent.setData((Uri.parse("custom://"+System.currentTimeMillis()))); - - return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); - } - - public boolean canReply() { - return notifications.size() >= 1 && notifications.get(0).canReply(); - } - -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/OptimizedMessageNotifier.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/OptimizedMessageNotifier.java index 406fd644c5..3151ea3d65 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/OptimizedMessageNotifier.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/OptimizedMessageNotifier.java @@ -11,7 +11,6 @@ import org.signal.core.util.concurrent.SignalExecutors; import org.thoughtcrime.securesms.notifications.v2.MessageNotifierV2; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.util.BubbleUtil; -import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.LeakyBucketLimiter; import org.thoughtcrime.securesms.util.Util; @@ -21,14 +20,11 @@ import org.thoughtcrime.securesms.util.Util; public class OptimizedMessageNotifier implements MessageNotifier { private final LeakyBucketLimiter limiter; - - private final DefaultMessageNotifier messageNotifierV1; - private final MessageNotifierV2 messageNotifierV2; + private final MessageNotifierV2 messageNotifierV2; @MainThread public OptimizedMessageNotifier(@NonNull Application context) { this.limiter = new LeakyBucketLimiter(5, 1000, new Handler(SignalExecutors.getAndStartHandlerThread("signal-notifier").getLooper())); - this.messageNotifierV1 = new DefaultMessageNotifier(); this.messageNotifierV2 = new MessageNotifierV2(context); } @@ -119,10 +115,6 @@ public class OptimizedMessageNotifier implements MessageNotifier { } private MessageNotifier getNotifier() { - if (FeatureFlags.useNewNotificationSystem()) { - return messageNotifierV2; - } else { - return messageNotifierV1; - } + return messageNotifierV2; } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/RemoteReplyReceiver.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/RemoteReplyReceiver.java index 599f057a8d..92237250d4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/RemoteReplyReceiver.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/RemoteReplyReceiver.java @@ -30,6 +30,7 @@ import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.MessageDatabase.MarkedMessageInfo; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.mms.OutgoingMediaMessage; +import org.thoughtcrime.securesms.notifications.v2.MessageNotifierV2; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.sms.MessageSender; @@ -61,7 +62,7 @@ public class RemoteReplyReceiver extends BroadcastReceiver { final RecipientId recipientId = intent.getParcelableExtra(RECIPIENT_EXTRA); final ReplyMethod replyMethod = (ReplyMethod) intent.getSerializableExtra(REPLY_METHOD); - final CharSequence responseText = remoteInput.getCharSequence(DefaultMessageNotifier.EXTRA_REMOTE_REPLY); + final CharSequence responseText = remoteInput.getCharSequence(MessageNotifierV2.EXTRA_REMOTE_REPLY); if (recipientId == null) throw new AssertionError("No recipientId specified"); if (replyMethod == null) throw new AssertionError("No reply method specified"); diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java deleted file mode 100644 index a659cd97a2..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java +++ /dev/null @@ -1,455 +0,0 @@ -package org.thoughtcrime.securesms.notifications; - -import android.app.Notification; -import android.app.PendingIntent; -import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.drawable.Drawable; -import android.net.Uri; -import android.os.Build; -import android.text.SpannableStringBuilder; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.StringRes; -import androidx.appcompat.view.ContextThemeWrapper; -import androidx.core.app.NotificationCompat; -import androidx.core.app.NotificationCompat.Action; -import androidx.core.app.Person; -import androidx.core.app.RemoteInput; -import androidx.core.graphics.drawable.IconCompat; - -import com.annimon.stream.Stream; -import com.bumptech.glide.load.MultiTransformation; -import com.bumptech.glide.load.Transformation; -import com.bumptech.glide.load.engine.DiskCacheStrategy; -import com.bumptech.glide.load.resource.bitmap.CircleCrop; - -import org.signal.core.util.logging.Log; -import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto; -import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto; -import org.thoughtcrime.securesms.contacts.avatars.GeneratedContactPhoto; -import org.thoughtcrime.securesms.conversation.ConversationIntents; -import org.thoughtcrime.securesms.conversation.colors.AvatarColor; -import org.thoughtcrime.securesms.database.DatabaseFactory; -import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; -import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader; -import org.thoughtcrime.securesms.mms.GlideApp; -import org.thoughtcrime.securesms.mms.Slide; -import org.thoughtcrime.securesms.mms.SlideDeck; -import org.thoughtcrime.securesms.preferences.widgets.NotificationPrivacyPreference; -import org.thoughtcrime.securesms.recipients.Recipient; -import org.thoughtcrime.securesms.util.AvatarUtil; -import org.thoughtcrime.securesms.util.BitmapUtil; -import org.thoughtcrime.securesms.util.BlurTransformation; -import org.thoughtcrime.securesms.util.BubbleUtil; -import org.thoughtcrime.securesms.util.ConversationUtil; -import org.thoughtcrime.securesms.util.TextSecurePreferences; -import org.thoughtcrime.securesms.util.Util; -import org.whispersystems.libsignal.util.guava.Optional; - -import java.util.ArrayList; -import java.util.LinkedList; -import java.util.List; -import java.util.concurrent.ExecutionException; - -public class SingleRecipientNotificationBuilder extends AbstractNotificationBuilder { - - private static final String TAG = Log.tag(SingleRecipientNotificationBuilder.class); - - private static final int BIG_PICTURE_DIMEN = 500; - private static final int LARGE_ICON_DIMEN = 250; - - private final List messages = new LinkedList<>(); - - private SlideDeck slideDeck; - private CharSequence contentTitle; - private CharSequence contentText; - private Recipient threadRecipient; - private BubbleUtil.BubbleState defaultBubbleState; - - public SingleRecipientNotificationBuilder(@NonNull Context context, @NonNull NotificationPrivacyPreference privacy) - { - super(new ContextThemeWrapper(context, R.style.TextSecure_LightTheme), privacy); - - setSmallIcon(R.drawable.ic_notification); - setColor(context.getResources().getColor(R.color.core_ultramarine)); - setCategory(NotificationCompat.CATEGORY_MESSAGE); - - if (!NotificationChannels.supported()) { - setPriority(TextSecurePreferences.getNotificationPriority(context)); - } - } - - public void setThread(@NonNull Recipient recipient) { - String channelId = recipient.getNotificationChannel(); - setChannelId(channelId != null ? channelId : NotificationChannels.getMessagesChannel(context)); - - if (privacy.isDisplayContact()) { - setContentTitle(recipient.getDisplayName(context)); - - if (recipient.getContactUri() != null) { - addPerson(recipient.getContactUri().toString()); - } - - setLargeIcon(getContactDrawable(recipient)); - - } else { - setContentTitle(context.getString(R.string.SingleRecipientNotificationBuilder_signal)); - setLargeIcon(new GeneratedContactPhoto("Unknown", R.drawable.ic_profile_outline_40).asDrawable(context, AvatarColor.UNKNOWN.colorInt())); - } - - setShortcutId(ConversationUtil.getShortcutId(recipient)); - } - - private Drawable getContactDrawable(@NonNull Recipient recipient) { - ContactPhoto contactPhoto = recipient.getContactPhoto(); - FallbackContactPhoto fallbackContactPhoto = recipient.getFallbackContactPhoto(); - - if (contactPhoto != null) { - try { - List> transforms = new ArrayList<>(); - if (recipient.shouldBlurAvatar()) { - transforms.add(new BlurTransformation(ApplicationDependencies.getApplication(), 0.25f, BlurTransformation.MAX_RADIUS)); - } - transforms.add(new CircleCrop()); - - return GlideApp.with(context.getApplicationContext()) - .load(contactPhoto) - .diskCacheStrategy(DiskCacheStrategy.ALL) - .transform(new MultiTransformation<>(transforms)) - .submit(context.getResources().getDimensionPixelSize(android.R.dimen.notification_large_icon_width), - context.getResources().getDimensionPixelSize(android.R.dimen.notification_large_icon_height)) - .get(); - } catch (InterruptedException | ExecutionException e) { - return fallbackContactPhoto.asDrawable(context, recipient.getAvatarColor().colorInt()); - } - } else { - return fallbackContactPhoto.asDrawable(context, recipient.getAvatarColor().colorInt()); - } - } - - public void setMessageCount(int messageCount) { - setContentInfo(String.valueOf(messageCount)); - setNumber(messageCount); - } - - public void setPrimaryMessageBody(@NonNull Recipient threadRecipients, - @NonNull Recipient individualRecipient, - @NonNull CharSequence message, - @Nullable SlideDeck slideDeck) - { - SpannableStringBuilder stringBuilder = new SpannableStringBuilder(); - - if (privacy.isDisplayContact() && threadRecipients.isGroup()) { - stringBuilder.append(Util.getBoldedString(individualRecipient.getDisplayName(context) + ": ")); - } - - if (privacy.isDisplayMessage()) { - setContentText(stringBuilder.append(message)); - this.slideDeck = slideDeck; - } else { - setContentText(stringBuilder.append(context.getString(R.string.SingleRecipientNotificationBuilder_new_message))); - } - } - - public void addAndroidAutoAction(@NonNull PendingIntent androidAutoReplyIntent, - @NonNull PendingIntent androidAutoHeardIntent, long timestamp) - { - - if (contentTitle == null || contentText == null) - return; - - RemoteInput remoteInput = new RemoteInput.Builder(AndroidAutoReplyReceiver.VOICE_REPLY_KEY) - .setLabel(context.getString(R.string.MessageNotifier_reply)) - .build(); - - NotificationCompat.CarExtender.UnreadConversation.Builder unreadConversationBuilder = - new NotificationCompat.CarExtender.UnreadConversation.Builder(contentTitle.toString()) - .addMessage(contentText.toString()) - .setLatestTimestamp(timestamp) - .setReadPendingIntent(androidAutoHeardIntent) - .setReplyAction(androidAutoReplyIntent, remoteInput); - - extend(new NotificationCompat.CarExtender().setUnreadConversation(unreadConversationBuilder.build())); - } - - public void addTurnOffTheseNotificationsAction(@NonNull PendingIntent turnOffTheseNotificationsIntent) { - Action turnOffTheseNotifications = new Action(R.drawable.check, - context.getString(R.string.MessageNotifier_turn_off_these_notifications), - turnOffTheseNotificationsIntent); - - addAction(turnOffTheseNotifications); - } - - public void addActions(@NonNull PendingIntent markReadIntent, - @NonNull PendingIntent quickReplyIntent, - @NonNull PendingIntent wearableReplyIntent, - @NonNull ReplyMethod replyMethod, - boolean replyEnabled) - { - NotificationCompat.WearableExtender extender = new NotificationCompat.WearableExtender(); - Action markAsReadAction = new Action(R.drawable.check, - context.getString(R.string.MessageNotifier_mark_read), - markReadIntent); - - addAction(markAsReadAction); - extender.addAction(markAsReadAction); - - if (replyEnabled) { - String actionName = context.getString(R.string.MessageNotifier_reply); - String label = context.getString(replyMethodLongDescription(replyMethod)); - - Action replyAction = new Action(R.drawable.ic_reply_white_36dp, - actionName, - quickReplyIntent); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - replyAction = new Action.Builder(R.drawable.ic_reply_white_36dp, - actionName, - wearableReplyIntent) - .addRemoteInput(new RemoteInput.Builder(DefaultMessageNotifier.EXTRA_REMOTE_REPLY) - .setLabel(label) - .build()) - .build(); - } - - Action wearableReplyAction = new Action.Builder(R.drawable.ic_reply, - actionName, - wearableReplyIntent) - .addRemoteInput(new RemoteInput.Builder(DefaultMessageNotifier.EXTRA_REMOTE_REPLY) - .setLabel(label) - .build()) - .build(); - - addAction(replyAction); - extend(extender.addAction(wearableReplyAction)); - } - } - - @StringRes - private static int replyMethodLongDescription(@NonNull ReplyMethod replyMethod) { - switch (replyMethod) { - case GroupMessage: - return R.string.MessageNotifier_reply; - case SecureMessage: - return R.string.MessageNotifier_signal_message; - case UnsecuredSmsMessage: - return R.string.MessageNotifier_unsecured_sms; - default: - return R.string.MessageNotifier_reply; - } - } - - public void addMessageBody(@NonNull Recipient threadRecipient, - @NonNull Recipient individualRecipient, - @Nullable CharSequence messageBody, - long timestamp, - @Nullable SlideDeck slideDeck) - { - SpannableStringBuilder stringBuilder = new SpannableStringBuilder(); - Person.Builder personBuilder = new Person.Builder() - .setKey(ConversationUtil.getShortcutId(individualRecipient)) - .setBot(false); - - this.threadRecipient = threadRecipient; - - if (privacy.isDisplayContact()) { - personBuilder.setName(individualRecipient.getDisplayName(context)); - personBuilder.setUri(individualRecipient.isSystemContact() ? individualRecipient.getContactUri().toString() : null); - - Bitmap bitmap = getLargeBitmap(getContactDrawable(individualRecipient)); - if (bitmap != null) { - personBuilder.setIcon(IconCompat.createWithBitmap(bitmap)); - } - } else { - personBuilder.setName(""); - } - - final CharSequence text; - if (privacy.isDisplayMessage()) { - text = messageBody == null ? "" : messageBody; - } else { - text = stringBuilder.append(context.getString(R.string.SingleRecipientNotificationBuilder_new_message)); - } - - Uri dataUri = null; - String mimeType = null; - - if (slideDeck != null && slideDeck.getThumbnailSlide() != null) { - Slide thumbnail = slideDeck.getThumbnailSlide(); - - if (Build.VERSION.SDK_INT >= 28) { - dataUri = thumbnail.getPublicUri(); - } else { - dataUri = thumbnail.getUri(); - } - - mimeType = thumbnail.getContentType(); - } - - messages.add(new NotificationCompat.MessagingStyle.Message(text, timestamp, personBuilder.build()).setData(mimeType, dataUri)); - } - - public void setDefaultBubbleState(@NonNull BubbleUtil.BubbleState bubbleState) { - this.defaultBubbleState = bubbleState; - } - - @Override - public Notification build() { - if (privacy.isDisplayMessage()) { - Optional largeIconUri = getLargeIconUri(slideDeck); - Optional bigPictureUri = getBigPictureUri(slideDeck); - - if (messages.size() == 1 && largeIconUri.isPresent()) { - setLargeIcon(getNotificationPicture(largeIconUri.get(), LARGE_ICON_DIMEN)); - } - - if (messages.size() == 1 && bigPictureUri.isPresent() && Build.VERSION.SDK_INT < ConversationUtil.CONVERSATION_SUPPORT_VERSION) { - setStyle(new NotificationCompat.BigPictureStyle() - .bigPicture(getNotificationPicture(bigPictureUri.get(), BIG_PICTURE_DIMEN)) - .setSummaryText(getBigText())); - } else { - if (Build.VERSION.SDK_INT >= 24) { - applyMessageStyle(); - - if (Build.VERSION.SDK_INT >= ConversationUtil.CONVERSATION_SUPPORT_VERSION) { - applyBubbleMetadata(); - } - } else { - applyLegacy(); - } - } - } - - return super.build(); - } - - private void applyMessageStyle() { - ConversationUtil.pushShortcutForRecipientIfNeededSync(context, threadRecipient); - NotificationCompat.MessagingStyle messagingStyle = new NotificationCompat.MessagingStyle(ConversationUtil.buildPersonCompat(context, Recipient.self())); - - if (threadRecipient.isGroup()) { - if (privacy.isDisplayContact()) { - messagingStyle.setConversationTitle(threadRecipient.getDisplayName(context)); - } else { - messagingStyle.setConversationTitle(context.getString(R.string.SingleRecipientNotificationBuilder_signal)); - } - - messagingStyle.setGroupConversation(true); - } - - Stream.of(messages).forEach(messagingStyle::addMessage); - setStyle(messagingStyle); - } - - private void applyBubbleMetadata() { - long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(threadRecipient); - PendingIntent intent = PendingIntent.getActivity(context, 0, ConversationIntents.createBubbleIntent(context, threadRecipient.getId(), threadId), 0); - NotificationCompat.BubbleMetadata bubbleMetadata = new NotificationCompat.BubbleMetadata.Builder() - .setAutoExpandBubble(defaultBubbleState == BubbleUtil.BubbleState.SHOWN) - .setDesiredHeight(600) - .setIcon(AvatarUtil.getIconCompatForShortcut(context, threadRecipient)) - .setSuppressNotification(defaultBubbleState == BubbleUtil.BubbleState.SHOWN) - .setIntent(intent) - .build(); - setBubbleMetadata(bubbleMetadata); - } - - private void applyLegacy() { - setStyle(new NotificationCompat.BigTextStyle().bigText(getBigText())); - } - - private void setLargeIcon(@Nullable Drawable drawable) { - if (drawable != null) { - setLargeIcon(getLargeBitmap(drawable)); - } - } - - private @Nullable Bitmap getLargeBitmap(@Nullable Drawable drawable) { - if (drawable != null) { - int largeIconTargetSize = context.getResources().getDimensionPixelSize(R.dimen.contact_photo_target_size); - - return BitmapUtil.createFromDrawable(drawable, largeIconTargetSize, largeIconTargetSize); - } - - return null; - } - - private static Optional getLargeIconUri(@Nullable SlideDeck slideDeck) { - if (slideDeck == null) { - return Optional.absent(); - } - - Slide thumbnailSlide = Optional.fromNullable(slideDeck.getThumbnailSlide()).or(Optional.fromNullable(slideDeck.getStickerSlide())).orNull(); - return getThumbnailUri(thumbnailSlide); - } - - private static Optional getBigPictureUri(@Nullable SlideDeck slideDeck) { - if (slideDeck == null) { - return Optional.absent(); - } - - Slide thumbnailSlide = slideDeck.getThumbnailSlide(); - return getThumbnailUri(thumbnailSlide); - } - - private static Optional getThumbnailUri(@Nullable Slide slide) { - if (slide != null && !slide.isInProgress() && slide.getUri() != null) { - return Optional.of(slide.getUri()); - } else { - return Optional.absent(); - } - } - - private Bitmap getNotificationPicture(@NonNull Uri uri, int dimension) - { - try { - return GlideApp.with(context.getApplicationContext()) - .asBitmap() - .load(new DecryptableStreamUriLoader.DecryptableUri(uri)) - .diskCacheStrategy(DiskCacheStrategy.NONE) - .submit(dimension, dimension) - .get(); - } catch (InterruptedException | ExecutionException e) { - Log.w(TAG, e); - return Bitmap.createBitmap(dimension, dimension, Bitmap.Config.RGB_565); - } - } - - @Override - public NotificationCompat.Builder setContentTitle(CharSequence contentTitle) { - this.contentTitle = contentTitle; - return super.setContentTitle(contentTitle); - } - - public NotificationCompat.Builder setContentText(CharSequence contentText) { - this.contentText = trimToDisplayLength(contentText); - return super.setContentText(this.contentText); - } - - private CharSequence getBigText() { - SpannableStringBuilder content = new SpannableStringBuilder(); - - for (int i = 0; i < messages.size(); i++) { - content.append(getBigTextFor(messages.get(i))); - if (i < messages.size() - 1) { - content.append('\n'); - } - } - - return content; - } - - private CharSequence getBigTextFor(NotificationCompat.MessagingStyle.Message message) { - SpannableStringBuilder content = new SpannableStringBuilder(); - - if (message.getPerson() != null && message.getPerson().getName() != null && threadRecipient.isGroup()) { - content.append(Util.getBoldedString(message.getPerson().getName().toString())).append(": "); - } - - return trimToDisplayLength(content.append(message.getText())); - } - -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/MessageNotifierV2.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/MessageNotifierV2.kt index ccb43afac1..88306b7835 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/MessageNotifierV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/MessageNotifierV2.kt @@ -18,7 +18,6 @@ import org.thoughtcrime.securesms.database.MessageDatabase import org.thoughtcrime.securesms.dependencies.ApplicationDependencies import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.messages.IncomingMessageObserver -import org.thoughtcrime.securesms.notifications.DefaultMessageNotifier import org.thoughtcrime.securesms.notifications.MessageNotifier import org.thoughtcrime.securesms.notifications.MessageNotifier.ReminderReceiver import org.thoughtcrime.securesms.notifications.NotificationCancellationHelper @@ -89,7 +88,7 @@ class MessageNotifierV2(context: Application) : MessageNotifier { } override fun updateNotification(context: Context, threadId: Long) { - if (System.currentTimeMillis() - lastDesktopActivityTimestamp < DefaultMessageNotifier.DESKTOP_ACTIVITY_PERIOD) { + if (System.currentTimeMillis() - lastDesktopActivityTimestamp < DESKTOP_ACTIVITY_PERIOD) { Log.i(TAG, "Scheduling delayed notification...") executor.enqueue(context, threadId) } else { @@ -264,7 +263,13 @@ class MessageNotifierV2(context: Application) : MessageNotifier { companion object { val TAG: String = Log.tag(MessageNotifierV2::class.java) + private val REMINDER_TIMEOUT: Long = TimeUnit.MINUTES.toMillis(2) + val MIN_AUDIBLE_PERIOD_MILLIS = TimeUnit.SECONDS.toMillis(2) + val DESKTOP_ACTIVITY_PERIOD = TimeUnit.MINUTES.toMillis(1) + + const val EXTRA_REMOTE_REPLY = "extra_remote_reply" + const val NOTIFICATION_GROUP = "messages" private fun updateBadge(context: Context, count: Int) { try { diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationBuilder.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationBuilder.kt index 42b71935f0..997a8e0af0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationBuilder.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationBuilder.kt @@ -1,19 +1,15 @@ package org.thoughtcrime.securesms.notifications.v2 -import android.annotation.TargetApi import android.app.Notification import android.app.PendingIntent -import android.app.Person import android.content.Context import android.graphics.Bitmap import android.graphics.Color -import android.graphics.drawable.Icon import android.net.Uri import android.os.Build import android.text.TextUtils import androidx.annotation.ColorInt import androidx.annotation.DrawableRes -import androidx.annotation.RequiresApi import androidx.annotation.StringRes import androidx.core.app.NotificationCompat import androidx.core.app.RemoteInput @@ -22,7 +18,6 @@ import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.conversation.ConversationIntents import org.thoughtcrime.securesms.database.RecipientDatabase import org.thoughtcrime.securesms.keyvalue.SignalStore -import org.thoughtcrime.securesms.notifications.DefaultMessageNotifier import org.thoughtcrime.securesms.notifications.NotificationChannels import org.thoughtcrime.securesms.notifications.ReplyMethod import org.thoughtcrime.securesms.preferences.widgets.NotificationPrivacyPreference @@ -171,7 +166,7 @@ sealed class NotificationBuilder(protected val context: Context) { /** * Notification builder using solely androidx/compat libraries. */ - class NotificationBuilderCompat(context: Context) : NotificationBuilder(context) { + private class NotificationBuilderCompat(context: Context) : NotificationBuilder(context) { val builder: NotificationCompat.Builder = NotificationCompat.Builder(context, NotificationChannels.getMessagesChannel(context)) override fun addActions(replyMethod: ReplyMethod, conversation: NotificationConversation) { @@ -194,7 +189,7 @@ sealed class NotificationBuilder(protected val context: Context) { val label: String = context.getString(replyMethod.toLongDescription()) val replyAction: NotificationCompat.Action = if (Build.VERSION.SDK_INT >= 24) { NotificationCompat.Action.Builder(R.drawable.ic_reply_white_36dp, actionName, remoteReply) - .addRemoteInput(RemoteInput.Builder(DefaultMessageNotifier.EXTRA_REMOTE_REPLY).setLabel(label).build()) + .addRemoteInput(RemoteInput.Builder(MessageNotifierV2.EXTRA_REMOTE_REPLY).setLabel(label).build()) .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY) .setShowsUserInterface(false) .build() @@ -203,7 +198,7 @@ sealed class NotificationBuilder(protected val context: Context) { } val wearableReplyAction = NotificationCompat.Action.Builder(R.drawable.ic_reply, actionName, remoteReply) - .addRemoteInput(RemoteInput.Builder(DefaultMessageNotifier.EXTRA_REMOTE_REPLY).setLabel(label).build()) + .addRemoteInput(RemoteInput.Builder(MessageNotifierV2.EXTRA_REMOTE_REPLY).setLabel(label).build()) .build() builder.addAction(replyAction) @@ -451,228 +446,6 @@ sealed class NotificationBuilder(protected val context: Context) { builder.setSubText(subText) } } - - /** - * Notification builder using solely on device OS libraries. - */ - @TargetApi(28) - class NotificationBuilderOS(context: Context) : NotificationBuilder(context) { - val builder: Notification.Builder = Notification.Builder(context, NotificationChannels.getMessagesChannel(context)) - - override fun addActions(replyMethod: ReplyMethod, conversation: NotificationConversation) { - val markAsRead: PendingIntent = conversation.getMarkAsReadIntent(context) - val markAsReadAction: Notification.Action = Notification.Action.Builder(context.getIcon(R.drawable.check), context.getString(R.string.MessageNotifier_mark_read), markAsRead) - .setSemanticAction(Notification.Action.SEMANTIC_ACTION_MARK_AS_READ) - .build() - val extender: Notification.WearableExtender = Notification.WearableExtender() - - builder.addAction(markAsReadAction) - extender.addAction(markAsReadAction) - - if (conversation.mostRecentNotification.canReply(context)) { - val remoteReply: PendingIntent = conversation.getRemoteReplyIntent(context, replyMethod) - - val actionName: String = context.getString(R.string.MessageNotifier_reply) - val label: String = context.getString(replyMethod.toLongDescription()) - val replyAction: Notification.Action = Notification.Action.Builder(context.getIcon(R.drawable.ic_reply_white_36dp), actionName, remoteReply) - .addRemoteInput(android.app.RemoteInput.Builder(DefaultMessageNotifier.EXTRA_REMOTE_REPLY).setLabel(label).build()) - .setSemanticAction(Notification.Action.SEMANTIC_ACTION_REPLY) - .build() - - val wearableReplyAction = Notification.Action.Builder(context.getIcon(R.drawable.ic_reply), actionName, remoteReply) - .addRemoteInput(android.app.RemoteInput.Builder(DefaultMessageNotifier.EXTRA_REMOTE_REPLY).setLabel(label).build()) - .build() - - builder.addAction(replyAction) - extender.addAction(wearableReplyAction) - } - - builder.extend(extender) - } - - override fun addMarkAsReadActionActual(state: NotificationStateV2) { - val markAllAsReadAction: Notification.Action = Notification.Action.Builder( - context.getIcon(R.drawable.check), - context.getString(R.string.MessageNotifier_mark_all_as_read), - state.getMarkAsReadIntent(context) - ).build() - - builder.addAction(markAllAsReadAction) - builder.extend(Notification.WearableExtender().addAction(markAllAsReadAction)) - } - - override fun addTurnOffJoinedNotificationsAction(pendingIntent: PendingIntent) { - val turnOffTheseNotifications: Notification.Action = Notification.Action.Builder( - context.getIcon(R.drawable.check), - context.getString(R.string.MessageNotifier_turn_off_these_notifications), - pendingIntent - ).build() - - builder.addAction(turnOffTheseNotifications) - } - - override fun addMessagesActual(conversation: NotificationConversation, includeShortcut: Boolean) { - val self: Person = Person.Builder() - .setBot(false) - .setName(if (includeShortcut) Recipient.self().getDisplayName(context) else context.getString(R.string.SingleRecipientNotificationBuilder_you)) - .setIcon(if (includeShortcut) Recipient.self().getContactDrawable(context).toLargeBitmap(context).toIcon() else null) - .setKey(ConversationUtil.getShortcutId(Recipient.self().id)) - .build() - - val messagingStyle: Notification.MessagingStyle = Notification.MessagingStyle(self) - messagingStyle.conversationTitle = conversation.getConversationTitle(context) - messagingStyle.isGroupConversation = conversation.isGroup - - conversation.notificationItems.forEach { notificationItem -> - val personBuilder: Person.Builder = Person.Builder() - .setBot(false) - .setName(notificationItem.getPersonName(context)) - .setUri(notificationItem.getPersonUri(context)) - .setIcon(notificationItem.getPersonIcon(context).toIcon()) - - if (includeShortcut) { - personBuilder.setKey(ConversationUtil.getShortcutId(notificationItem.individualRecipient)) - } - - val (dataUri: Uri?, mimeType: String?) = notificationItem.getThumbnailInfo(context) - - messagingStyle.addMessage(Notification.MessagingStyle.Message(notificationItem.getPrimaryText(context), notificationItem.timestamp, personBuilder.build()).setData(mimeType, dataUri)) - } - - builder.style = messagingStyle - } - - override fun setBubbleMetadataActual(conversation: NotificationConversation, bubbleState: BubbleUtil.BubbleState) { - if (Build.VERSION.SDK_INT < ConversationUtil.CONVERSATION_SUPPORT_VERSION) { - return - } - - val intent = PendingIntent.getActivity( - context, - 0, - ConversationIntents.createBubbleIntent(context, conversation.recipient.id, conversation.threadId), - 0 - ) - - val bubbleMetadata = Notification.BubbleMetadata.Builder(intent, AvatarUtil.getIconForShortcut(context, conversation.recipient)) - .setAutoExpandBubble(bubbleState === BubbleUtil.BubbleState.SHOWN) - .setDesiredHeight(600) - .setSuppressNotification(bubbleState === BubbleUtil.BubbleState.SHOWN) - .build() - - builder.setBubbleMetadata(bubbleMetadata) - } - - override fun addMessagesActual(state: NotificationStateV2) { - // Intentionally left blank - } - - override fun setLights(@ColorInt color: Int, onTime: Int, offTime: Int) { - // Intentionally left blank - } - - override fun setAlarms(recipient: Recipient?) { - // Intentionally left blank - } - - override fun setSmallIcon(drawable: Int) { - builder.setSmallIcon(drawable) - } - - override fun setColor(@ColorInt color: Int) { - builder.setColor(color) - } - - override fun setCategory(category: String) { - builder.setCategory(category) - } - - override fun setGroup(group: String) { - builder.setGroup(group) - } - - override fun setGroupAlertBehavior(behavior: Int) { - builder.setGroupAlertBehavior(behavior) - } - - override fun setChannelId(channelId: String) { - builder.setChannelId(channelId) - } - - override fun setContentTitle(contentTitle: CharSequence) { - builder.setContentTitle(contentTitle) - } - - override fun setLargeIcon(largeIcon: Bitmap?) { - builder.setLargeIcon(largeIcon) - } - - override fun setShortcutIdActual(shortcutId: String) { - builder.setShortcutId(shortcutId) - } - - @Suppress("DEPRECATION") - override fun setContentInfo(contentInfo: String) { - builder.setContentInfo(contentInfo) - } - - override fun setNumber(number: Int) { - builder.setNumber(number) - } - - override fun setContentText(contentText: CharSequence?) { - builder.setContentText(contentText) - } - - override fun setTicker(ticker: CharSequence?) { - builder.setTicker(ticker) - } - - override fun setContentIntent(pendingIntent: PendingIntent?) { - builder.setContentIntent(pendingIntent) - } - - override fun setDeleteIntent(deleteIntent: PendingIntent?) { - builder.setDeleteIntent(deleteIntent) - } - - override fun setSortKey(sortKey: String) { - builder.setSortKey(sortKey) - } - - override fun setOnlyAlertOnce(onlyAlertOnce: Boolean) { - builder.setOnlyAlertOnce(onlyAlertOnce) - } - - override fun setPriority(priority: Int) { - // Intentionally left blank - } - - override fun setAutoCancel(autoCancel: Boolean) { - builder.setAutoCancel(autoCancel) - } - - override fun build(): Notification { - return builder.build() - } - - override fun addPersonActual(recipient: Recipient) { - builder.addPerson(ConversationUtil.buildPerson(context, recipient)) - } - - override fun setWhen(timestamp: Long) { - builder.setWhen(timestamp) - builder.setShowWhen(true) - } - - override fun setGroupSummary(isGroupSummary: Boolean) { - builder.setGroupSummary(isGroupSummary) - } - - override fun setSubText(subText: String) { - builder.setSubText(subText) - } - } } private fun Bitmap?.toIconCompat(): IconCompat? { @@ -683,20 +456,6 @@ private fun Bitmap?.toIconCompat(): IconCompat? { } } -@RequiresApi(23) -private fun Bitmap?.toIcon(): Icon? { - return if (this != null) { - Icon.createWithBitmap(this) - } else { - null - } -} - -@RequiresApi(23) -private fun Context.getIcon(@DrawableRes drawableRes: Int): Icon { - return Icon.createWithResource(this, drawableRes) -} - @StringRes private fun ReplyMethod.toLongDescription(): Int { return when (this) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationFactory.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationFactory.kt index 98c22fccb3..0ead43a1ce 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationFactory.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationFactory.kt @@ -21,8 +21,8 @@ import org.thoughtcrime.securesms.MainActivity import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.conversation.ConversationIntents import org.thoughtcrime.securesms.database.DatabaseFactory +import org.thoughtcrime.securesms.database.model.InMemoryMessageRecord import org.thoughtcrime.securesms.keyvalue.SignalStore -import org.thoughtcrime.securesms.notifications.DefaultMessageNotifier import org.thoughtcrime.securesms.notifications.NotificationChannels import org.thoughtcrime.securesms.notifications.NotificationIds import org.thoughtcrime.securesms.recipients.Recipient @@ -181,7 +181,7 @@ object NotificationFactory { setSmallIcon(R.drawable.ic_notification) setColor(ContextCompat.getColor(context, R.color.core_ultramarine)) setCategory(NotificationCompat.CATEGORY_MESSAGE) - setGroup(DefaultMessageNotifier.NOTIFICATION_GROUP) + setGroup(MessageNotifierV2.NOTIFICATION_GROUP) setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN) setChannelId(conversation.getChannelId(context)) setContentTitle(conversation.getContentTitle(context)) @@ -225,7 +225,7 @@ object NotificationFactory { setSmallIcon(R.drawable.ic_notification) setColor(ContextCompat.getColor(context, R.color.core_ultramarine)) setCategory(NotificationCompat.CATEGORY_MESSAGE) - setGroup(DefaultMessageNotifier.NOTIFICATION_GROUP) + setGroup(MessageNotifierV2.NOTIFICATION_GROUP) setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN) setChannelId(NotificationChannels.getMessagesChannel(context)) setContentTitle(context.getString(R.string.app_name)) @@ -253,7 +253,7 @@ object NotificationFactory { private fun notifyInThread(context: Context, recipient: Recipient, lastAudibleNotification: Long) { if (!SignalStore.settings().isMessageNotificationsInChatSoundsEnabled || ServiceUtil.getAudioManager(context).ringerMode != AudioManager.RINGER_MODE_NORMAL || - (System.currentTimeMillis() - lastAudibleNotification) < DefaultMessageNotifier.MIN_AUDIBLE_PERIOD_MILLIS + (System.currentTimeMillis() - lastAudibleNotification) < MessageNotifierV2.MIN_AUDIBLE_PERIOD_MILLIS ) { return } @@ -343,6 +343,38 @@ object NotificationFactory { NotificationManagerCompat.from(context).safelyNotify(context, recipient, threadId.toInt(), builder.build()) } + @JvmStatic + fun notifyToBubbleConversation(context: Context, recipient: Recipient, threadId: Long) { + val builder: NotificationBuilder = NotificationBuilder.create(context) + + val conversation = NotificationConversation( + recipient = recipient, + threadId = threadId, + notificationItems = listOf( + MessageNotification( + threadRecipient = recipient, + record = InMemoryMessageRecord.ForceConversationBubble(recipient, threadId) + ) + ) + ) + + builder.apply { + setSmallIcon(R.drawable.ic_notification) + setColor(ContextCompat.getColor(context, R.color.core_ultramarine)) + setCategory(NotificationCompat.CATEGORY_MESSAGE) + setChannelId(conversation.getChannelId(context)) + setContentTitle(conversation.getContentTitle(context)) + setLargeIcon(conversation.getContactLargeIcon(context).toLargeBitmap(context)) + addPerson(conversation.recipient) + setShortcutId(ConversationUtil.getShortcutId(conversation.recipient)) + addMessages(conversation) + setBubbleMetadata(conversation, BubbleUtil.BubbleState.SHOWN) + } + + Log.d(TAG, "Posting Notification for requested bubble") + NotificationManagerCompat.from(context).safelyNotify(context, recipient, conversation.notificationId, builder.build()) + } + private fun NotificationManagerCompat.safelyNotify(context: Context, threadRecipient: Recipient?, notificationId: Int, notification: Notification) { try { notify(notificationId, notification) diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationItemV2.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationItemV2.kt index aba04d7f58..35c8442b44 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationItemV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationItemV2.kt @@ -19,7 +19,6 @@ import org.thoughtcrime.securesms.database.model.ReactionRecord import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.mms.Slide import org.thoughtcrime.securesms.mms.SlideDeck -import org.thoughtcrime.securesms.notifications.AbstractNotificationBuilder import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.RecipientUtil import org.thoughtcrime.securesms.service.KeyCachingService @@ -30,6 +29,7 @@ import org.thoughtcrime.securesms.util.Util private val TAG: String = Log.tag(NotificationItemV2::class.java) private const val EMOJI_REPLACEMENT_STRING = "__EMOJI__" +private const val MAX_DISPLAY_LENGTH = 500 /** * Base for messaged-based notifications. Represents a single notification. @@ -145,10 +145,10 @@ sealed class NotificationItemV2(val threadRecipient: Recipient, protected val re private fun CharSequence?.trimToDisplayLength(): CharSequence { val text: CharSequence = this ?: "" - return if (text.length <= AbstractNotificationBuilder.MAX_DISPLAY_LENGTH) { + return if (text.length <= MAX_DISPLAY_LENGTH) { text } else { - text.subSequence(0, AbstractNotificationBuilder.MAX_DISPLAY_LENGTH) + text.subSequence(0, MAX_DISPLAY_LENGTH) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/BubbleUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/BubbleUtil.java index 7f95cb6ab6..30f6d2d76d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/BubbleUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/BubbleUtil.java @@ -18,9 +18,8 @@ import org.signal.core.util.concurrent.SignalExecutors; import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.keyvalue.SignalStore; -import org.thoughtcrime.securesms.notifications.DefaultMessageNotifier; import org.thoughtcrime.securesms.notifications.NotificationIds; -import org.thoughtcrime.securesms.notifications.SingleRecipientNotificationBuilder; +import org.thoughtcrime.securesms.notifications.v2.NotificationFactory; import org.thoughtcrime.securesms.preferences.widgets.NotificationPrivacyPreference; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; @@ -90,16 +89,8 @@ public final class BubbleUtil { if (activeThreadNotification != null && activeThreadNotification.deleteIntent != null) { ApplicationDependencies.getMessageNotifier().updateNotification(context, threadId, BubbleState.SHOWN); } else { - Recipient recipient = Recipient.resolved(recipientId); - SingleRecipientNotificationBuilder builder = new SingleRecipientNotificationBuilder(context, SignalStore.settings().getMessageNotificationsPrivacy()); - - builder.addMessageBody(recipient, recipient, "", System.currentTimeMillis(), null); - builder.setThread(recipient); - builder.setDefaultBubbleState(BubbleState.SHOWN); - builder.setGroup(DefaultMessageNotifier.NOTIFICATION_GROUP); - - Log.d(TAG, "Posting Notification for requested bubble"); - notificationManager.notify(NotificationIds.getNotificationIdForThread(threadId), builder.build()); + Recipient recipient = Recipient.resolved(recipientId); + NotificationFactory.notifyToBubbleConversation(context, recipient, threadId); } } }); diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java index 4a15f52ac4..81e6f2999c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java @@ -1,7 +1,5 @@ package org.thoughtcrime.securesms.util; -import android.app.Application; -import android.os.Build; import android.text.TextUtils; import androidx.annotation.NonNull; @@ -77,7 +75,6 @@ public final class FeatureFlags { private static final String ANIMATED_STICKER_MIN_TOTAL_MEMORY = "android.animatedStickerMinTotalMemory"; private static final String MESSAGE_PROCESSOR_ALARM_INTERVAL = "android.messageProcessor.alarmIntervalMins"; private static final String MESSAGE_PROCESSOR_DELAY = "android.messageProcessor.foregroundDelayMs"; - private static final String NOTIFICATION_REWRITE = "android.notificationRewrite"; private static final String MP4_GIF_SEND_SUPPORT = "android.mp4GifSendSupport"; private static final String MEDIA_QUALITY_LEVELS = "android.mediaQuality.levels"; private static final String RETRY_RECEIPT_LIFESPAN = "android.retryReceiptLifespan"; @@ -114,7 +111,6 @@ public final class FeatureFlags { ANIMATED_STICKER_MIN_TOTAL_MEMORY, MESSAGE_PROCESSOR_ALARM_INTERVAL, MESSAGE_PROCESSOR_DELAY, - NOTIFICATION_REWRITE, MP4_GIF_SEND_SUPPORT, MEDIA_QUALITY_LEVELS, RETRY_RECEIPT_LIFESPAN, @@ -163,7 +159,6 @@ public final class FeatureFlags { MESSAGE_PROCESSOR_ALARM_INTERVAL, MESSAGE_PROCESSOR_DELAY, GV1_FORCED_MIGRATE, - NOTIFICATION_REWRITE, MP4_GIF_SEND_SUPPORT, MEDIA_QUALITY_LEVELS, RETRY_RECEIPT_LIFESPAN, @@ -354,11 +349,6 @@ public final class FeatureFlags { return getInteger(ANIMATED_STICKER_MIN_TOTAL_MEMORY, (int) ByteUnit.GIGABYTES.toMegabytes(3)); } - /** Whether or not to use the new notification system. */ - public static boolean useNewNotificationSystem() { - return Build.VERSION.SDK_INT >= 26 || getBoolean(NOTIFICATION_REWRITE, false); - } - public static boolean mp4GifSendSupport() { return getBoolean(MP4_GIF_SEND_SUPPORT, false); }