mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-23 19:26:17 +00:00
Remove old notification system and notification rewrite feature flag.
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<Void, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
List<MarkedMessageInfo> messageIdsCollection = new LinkedList<>();
|
||||
|
||||
for (long threadId : threadIds) {
|
||||
Log.i(TAG, "Marking meassage as read: " + threadId);
|
||||
List<MarkedMessageInfo> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<Void, Void, Void>() {
|
||||
@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<MarkedMessageInfo> 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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<Long> smsIds = new LinkedList<>();
|
||||
List<Long> 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<NotificationItem> 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<NotificationItem> 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<NotificationItem> 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<NotificationItem> 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<DelayedNotification> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -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<CharSequence> 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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<NotificationItem> notificationItemComparator = (a, b) -> -Long.compare(a.getTimestamp(), b.getTimestamp());
|
||||
private final List<NotificationItem> notifications = new LinkedList<>();
|
||||
private final LinkedHashSet<Long> threads = new LinkedHashSet<>();
|
||||
|
||||
public NotificationState() {}
|
||||
|
||||
public NotificationState(@NonNull List<NotificationItem> 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<Long> getThreads() {
|
||||
return threads;
|
||||
}
|
||||
|
||||
public int getThreadCount() {
|
||||
return threads.size();
|
||||
}
|
||||
|
||||
public int getMessageCount() {
|
||||
return notifications.size();
|
||||
}
|
||||
|
||||
public List<NotificationItem> getNotifications() {
|
||||
return notifications;
|
||||
}
|
||||
|
||||
public List<NotificationItem> getNotificationsForThread(long threadId) {
|
||||
List<NotificationItem> 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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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<NotificationCompat.MessagingStyle.Message> 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<Transformation<Bitmap>> 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<Uri> largeIconUri = getLargeIconUri(slideDeck);
|
||||
Optional<Uri> 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<Uri> 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<Uri> getBigPictureUri(@Nullable SlideDeck slideDeck) {
|
||||
if (slideDeck == null) {
|
||||
return Optional.absent();
|
||||
}
|
||||
|
||||
Slide thumbnailSlide = slideDeck.getThumbnailSlide();
|
||||
return getThumbnailUri(thumbnailSlide);
|
||||
}
|
||||
|
||||
private static Optional<Uri> 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()));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user