Add new notification system.

fork-5.53.8
Cody Henthorne 2021-04-13 17:12:54 -04:00 zatwierdzone przez Greyson Parrelli
rodzic c8f17e2ab0
commit e796968d19
23 zmienionych plików z 2030 dodań i 56 usunięć

4
.editorconfig 100644
Wyświetl plik

@ -0,0 +1,4 @@
root = true
[*.kt]
indent_size = 2

Wyświetl plik

@ -3,9 +3,12 @@ import org.signal.signing.ApkSignerUtil
import java.security.MessageDigest
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply plugin: 'com.google.protobuf'
apply plugin: 'androidx.navigation.safeargs'
apply plugin: 'witness'
apply plugin: 'org.jlleitschuh.gradle.ktlint'
apply from: 'translations.gradle'
apply from: 'witness-verifications.gradle'
@ -388,8 +391,8 @@ dependencies {
implementation 'org.apache.httpcomponents:httpclient-android:4.3.5'
implementation 'com.github.chrisbanes:PhotoView:2.1.3'
implementation 'com.github.bumptech.glide:glide:4.11.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'
annotationProcessor 'androidx.annotation:annotation:1.1.0'
kapt 'com.github.bumptech.glide:compiler:4.11.0'
kapt 'androidx.annotation:annotation:1.1.0'
implementation 'com.makeramen:roundedimageview:2.1.0'
implementation 'com.pnikosis:materialish-progress:1.5'
implementation 'org.greenrobot:eventbus:3.0.0'
@ -449,6 +452,8 @@ dependencies {
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
}
dependencyVerification {

Wyświetl plik

@ -40,7 +40,6 @@ import java.io.Closeable;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
@ -744,11 +743,11 @@ public class MmsSmsDatabase extends Database {
return db.rawQuery(query, null);
}
public Reader readerFor(@NonNull Cursor cursor) {
public static Reader readerFor(@NonNull Cursor cursor) {
return new Reader(cursor);
}
public class Reader implements Closeable {
public static class Reader implements Closeable {
private final Cursor cursor;
private SmsDatabase.Reader smsReader;

Wyświetl plik

@ -36,6 +36,7 @@ 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;
@ -185,7 +186,14 @@ public class ApplicationDependencyProvider implements ApplicationDependencies.Pr
@Override
public @NonNull MessageNotifier provideMessageNotifier() {
return new OptimizedMessageNotifier(new DefaultMessageNotifier());
MessageNotifier inner;
if (FeatureFlags.useNewNotificationSystem()) {
inner = new MessageNotifierV2();
} else {
inner = new DefaultMessageNotifier();
}
return new OptimizedMessageNotifier(inner);
}
@Override

Wyświetl plik

@ -19,6 +19,7 @@ package org.thoughtcrime.securesms.mms;
import android.content.Context;
import android.content.res.Resources.Theme;
import android.net.Uri;
import android.os.Build;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
@ -56,7 +57,11 @@ public abstract class Slide {
}
public @Nullable Uri getPublicUri() {
return attachment.getPublicUri();
if (Build.VERSION.SDK_INT >= 28) {
return attachment.getPublicUri();
} else {
return attachment.getUri();
}
}
@NonNull

Wyświetl plik

@ -24,7 +24,7 @@ public abstract class AbstractNotificationBuilder extends NotificationCompat.Bui
@SuppressWarnings("unused")
private static final String TAG = Log.tag(AbstractNotificationBuilder.class);
private static final int MAX_DISPLAY_LENGTH = 500;
public static final int MAX_DISPLAY_LENGTH = 500;
protected Context context;
protected NotificationPrivacyPreference privacy;

Wyświetl plik

@ -102,8 +102,8 @@ public class DefaultMessageNotifier implements MessageNotifier {
public static final String NOTIFICATION_GROUP = "messages";
private static final String EMOJI_REPLACEMENT_STRING = "__EMOJI__";
private static final long MIN_AUDIBLE_PERIOD_MILLIS = TimeUnit.SECONDS.toMillis(2);
private static final long DESKTOP_ACTIVITY_PERIOD = TimeUnit.MINUTES.toMillis(1);
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;

Wyświetl plik

@ -43,7 +43,7 @@ public final class NotificationCancellationHelper {
* 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.
*/
static void cancelAllMessageNotifications(@NonNull Context context) {
public static void cancelAllMessageNotifications(@NonNull Context context) {
if (Build.VERSION.SDK_INT >= 23) {
try {
NotificationManager notifications = ServiceUtil.getNotificationManager(context);

Wyświetl plik

@ -1,40 +0,0 @@
package org.thoughtcrime.securesms.notifications;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import androidx.core.app.NotificationCompat;
import org.thoughtcrime.securesms.MainActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.preferences.widgets.NotificationPrivacyPreference;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
public class PendingMessageNotificationBuilder extends AbstractNotificationBuilder {
public PendingMessageNotificationBuilder(Context context, NotificationPrivacyPreference privacy) {
super(context, privacy);
setSmallIcon(R.drawable.ic_notification);
setColor(context.getResources().getColor(R.color.core_ultramarine));
setCategory(NotificationCompat.CATEGORY_MESSAGE);
setContentTitle(context.getString(R.string.MessageNotifier_you_may_have_new_messages));
setContentText(context.getString(R.string.MessageNotifier_open_signal_to_check_for_recent_notifications));
setTicker(context.getString(R.string.MessageNotifier_open_signal_to_check_for_recent_notifications));
// TODO [greyson] Navigation
setContentIntent(PendingIntent.getActivity(context, 0, MainActivity.clearTop(context), 0));
setAutoCancel(true);
setAlarms(null, RecipientDatabase.VibrateState.DEFAULT);
setOnlyAlertOnce(true);
if (!NotificationChannels.supported()) {
setPriority(TextSecurePreferences.getNotificationPriority(context));
}
}
}

Wyświetl plik

@ -0,0 +1,308 @@
package org.thoughtcrime.securesms.notifications.v2
import android.app.AlarmManager
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Build
import android.service.notification.StatusBarNotification
import androidx.appcompat.view.ContextThemeWrapper
import androidx.core.content.ContextCompat
import me.leolin.shortcutbadger.ShortcutBadger
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
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
import org.thoughtcrime.securesms.notifications.NotificationIds
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.service.KeyCachingService
import org.thoughtcrime.securesms.util.BubbleUtil.BubbleState
import org.thoughtcrime.securesms.util.FeatureFlags
import org.thoughtcrime.securesms.util.ServiceUtil
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.thoughtcrime.securesms.webrtc.CallNotificationBuilder
import org.whispersystems.signalservice.internal.util.Util
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.Executor
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.collections.MutableMap.MutableEntry
import kotlin.math.max
/**
* MessageNotifier implementation using the new system for creating and showing notifications.
*/
class MessageNotifierV2 : MessageNotifier {
@Volatile private var visibleThread: Long = -1
@Volatile private var lastDesktopActivityTimestamp: Long = -1
@Volatile private var lastAudibleNotification: Long = -1
@Volatile private var lastScheduledReminder: Long = 0
private val threadReminders: MutableMap<Long, Reminder> = ConcurrentHashMap()
private val executor = CancelableExecutor()
override fun setVisibleThread(threadId: Long) {
visibleThread = threadId
}
override fun getVisibleThread(): Long {
return visibleThread
}
override fun clearVisibleThread() {
setVisibleThread(-1)
}
override fun setLastDesktopActivityTimestamp(timestamp: Long) {
lastDesktopActivityTimestamp = timestamp
}
override fun notifyMessageDeliveryFailed(context: Context, recipient: Recipient, threadId: Long) {
NotificationFactory.notifyMessageDeliveryFailed(context, recipient, threadId, visibleThread)
}
override fun cancelDelayedNotifications() {
executor.cancel()
}
override fun updateNotification(context: Context) {
updateNotification(context, -1, false, 0, BubbleState.HIDDEN)
}
override fun updateNotification(context: Context, threadId: Long) {
if (System.currentTimeMillis() - lastDesktopActivityTimestamp < DefaultMessageNotifier.DESKTOP_ACTIVITY_PERIOD) {
Log.i(TAG, "Scheduling delayed notification...")
executor.enqueue(context, threadId)
} else {
updateNotification(context, threadId, true)
}
}
override fun updateNotification(context: Context, threadId: Long, defaultBubbleState: BubbleState) {
updateNotification(context, threadId, false, 0, defaultBubbleState)
}
override fun updateNotification(context: Context, threadId: Long, signal: Boolean) {
updateNotification(context, threadId, signal, 0, BubbleState.HIDDEN)
}
/**
* @param signal is no longer used
* @param reminderCount is not longer used
*/
override fun updateNotification(
context: Context,
threadId: Long,
signal: Boolean,
reminderCount: Int,
defaultBubbleState: BubbleState
) {
if (!TextSecurePreferences.isNotificationsEnabled(context)) {
return
}
val state: NotificationStateV2 = NotificationStateProvider.constructNotificationState(context)
if (FeatureFlags.internalUser()) {
Log.i(TAG, state.toString())
}
if (state.isEmpty) {
Log.i(TAG, "State is empty, cancelling all notifications")
NotificationCancellationHelper.cancelAllMessageNotifications(context)
updateBadge(context, 0)
clearReminderInternal(context)
return
}
val alertOverrides: Set<Long> = threadReminders.filter { (_, reminder) -> reminder.lastNotified < System.currentTimeMillis() - REMINDER_TIMEOUT }.keys
val threadsThatAlerted: Set<Long> = NotificationFactory.notify(
context = ContextThemeWrapper(context, R.style.TextSecure_LightTheme),
state = state,
visibleThreadId = visibleThread,
targetThreadId = threadId,
defaultBubbleState = defaultBubbleState,
lastAudibleNotification = lastAudibleNotification,
alertOverrides = alertOverrides
)
lastAudibleNotification = System.currentTimeMillis()
updateReminderTimestamps(context, alertOverrides, threadsThatAlerted)
ServiceUtil.getNotificationManager(context).cancelOrphanedNotifications(context, state)
updateBadge(context, state.messageCount)
val smsIds: MutableList<Long> = mutableListOf()
val mmsIds: MutableList<Long> = mutableListOf()
for (item: NotificationItemV2 in state.notificationItems) {
if (item.isMms) {
mmsIds.add(item.id)
} else {
smsIds.add(item.id)
}
}
DatabaseFactory.getMmsSmsDatabase(context).setNotifiedTimestamp(System.currentTimeMillis(), smsIds, mmsIds)
Log.i(TAG, "threads: ${state.threadCount} messages: ${state.messageCount}")
}
private fun updateReminderTimestamps(context: Context, alertOverrides: Set<Long>, threadsThatAlerted: Set<Long>) {
if (TextSecurePreferences.getRepeatAlertsCount(context) == 0) {
return
}
val iterator: MutableIterator<MutableEntry<Long, Reminder>> = threadReminders.iterator()
while (iterator.hasNext()) {
val entry: MutableEntry<Long, Reminder> = iterator.next()
val (id: Long, reminder: Reminder) = entry
if (alertOverrides.contains(id)) {
val notifyCount: Int = reminder.count + 1
if (notifyCount >= TextSecurePreferences.getRepeatAlertsCount(context)) {
iterator.remove()
} else {
entry.setValue(Reminder(lastAudibleNotification, notifyCount))
}
}
}
for (alertedThreadId: Long in threadsThatAlerted) {
threadReminders[alertedThreadId] = Reminder(lastAudibleNotification)
}
if (threadReminders.isNotEmpty()) {
scheduleReminder(context)
} else {
lastScheduledReminder = 0
}
}
private fun scheduleReminder(context: Context) {
val timeout: Long = if (lastScheduledReminder != 0L) {
max(TimeUnit.SECONDS.toMillis(5), REMINDER_TIMEOUT - (System.currentTimeMillis() - lastScheduledReminder))
} else {
REMINDER_TIMEOUT
}
val alarmManager: AlarmManager? = ContextCompat.getSystemService(context, AlarmManager::class.java)
val pendingIntent: PendingIntent = PendingIntent.getBroadcast(context, 0, Intent(context, ReminderReceiver::class.java), PendingIntent.FLAG_UPDATE_CURRENT)
alarmManager?.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + timeout, pendingIntent)
lastScheduledReminder = System.currentTimeMillis()
}
private fun clearReminderInternal(context: Context) {
lastScheduledReminder = 0
threadReminders.clear()
val pendingIntent: PendingIntent = PendingIntent.getBroadcast(context, 0, Intent(context, ReminderReceiver::class.java), PendingIntent.FLAG_CANCEL_CURRENT)
val alarmManager: AlarmManager? = ContextCompat.getSystemService(context, AlarmManager::class.java)
alarmManager?.cancel(pendingIntent)
}
override fun clearReminder(context: Context) {
// Intentionally left blank
}
companion object {
private val TAG = Log.tag(MessageNotifierV2::class.java)
private val REMINDER_TIMEOUT = TimeUnit.MINUTES.toMillis(2)
private fun updateBadge(context: Context, count: Int) {
try {
if (count == 0) ShortcutBadger.removeCount(context) else ShortcutBadger.applyCount(context, count)
} catch (t: Throwable) {
Log.w(TAG, t)
}
}
}
private fun NotificationManager.cancelOrphanedNotifications(context: Context, state: NotificationStateV2) {
if (Build.VERSION.SDK_INT < 23) {
return
}
try {
for (notification: StatusBarNotification in activeNotifications) {
if (notification.id != NotificationIds.MESSAGE_SUMMARY &&
notification.id != KeyCachingService.SERVICE_RUNNING_ID &&
notification.id != IncomingMessageObserver.FOREGROUND_ID &&
notification.id != NotificationIds.PENDING_MESSAGES &&
!CallNotificationBuilder.isWebRtcNotification(notification.id)
) {
if (!state.notificationIds.contains(notification.id)) {
Log.d(TAG, "Cancelling orphaned notification: ${notification.id}")
NotificationCancellationHelper.cancel(context, notification.id)
}
}
}
} catch (e: Throwable) {
Log.w(TAG, e)
}
}
private data class Reminder(val lastNotified: Long, val count: Int = 0)
}
private class CancelableExecutor {
private val executor: Executor = Executors.newSingleThreadExecutor()
private val tasks: MutableSet<DelayedNotification> = mutableSetOf()
fun enqueue(context: Context, threadId: Long) {
execute(DelayedNotification(context, threadId))
}
private fun execute(runnable: DelayedNotification) {
synchronized(tasks) { tasks.add(runnable) }
val wrapper = Runnable {
runnable.run()
synchronized(tasks) { tasks.remove(runnable) }
}
executor.execute(wrapper)
}
fun cancel() {
synchronized(tasks) {
for (task in tasks) {
task.cancel()
}
}
}
private class DelayedNotification constructor(private val context: Context, private val threadId: Long) : Runnable {
private val canceled = AtomicBoolean(false)
private val delayUntil: Long = System.currentTimeMillis() + DELAY
override fun run() {
val 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...")
}
}
fun cancel() {
canceled.set(true)
}
companion object {
private val DELAY = TimeUnit.SECONDS.toMillis(5)
private val TAG = Log.tag(DelayedNotification::class.java)
}
}
}

Wyświetl plik

@ -0,0 +1,649 @@
package org.thoughtcrime.securesms.notifications.v2
import android.annotation.TargetApi
import android.app.Notification
import android.app.PendingIntent
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.Person
import androidx.core.app.RemoteInput
import androidx.core.graphics.drawable.IconCompat
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.conversation.ConversationIntents
import org.thoughtcrime.securesms.database.RecipientDatabase
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
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientUtil
import org.thoughtcrime.securesms.service.KeyCachingService
import org.thoughtcrime.securesms.util.AvatarUtil
import org.thoughtcrime.securesms.util.BubbleUtil
import org.thoughtcrime.securesms.util.ConversationUtil
import org.thoughtcrime.securesms.util.TextSecurePreferences
private const val BIG_PICTURE_DIMEN = 500
/**
* Wraps the compat and OS versions of the Notification builders so we can more easily access native
* features in newer versions. Also provides some domain specific helpers.
*
* Note: All business logic should exist in the base builder or the models that drive the notifications
* like NotificationConversation and NotificationItemV2.
*/
sealed class NotificationBuilder(protected val context: Context) {
private val privacy: NotificationPrivacyPreference = TextSecurePreferences.getNotificationPrivacy(context)
abstract fun setSmallIcon(@DrawableRes drawable: Int)
abstract fun setColor(@ColorInt color: Int)
abstract fun setCategory(category: String)
abstract fun setGroup(group: String)
abstract fun setGroupAlertBehavior(behavior: Int)
abstract fun setChannelId(channelId: String)
abstract fun setContentTitle(contentTitle: CharSequence)
abstract fun setLargeIcon(largeIcon: Bitmap?)
abstract fun setShortcutId(shortcutId: String)
abstract fun setContentInfo(contentInfo: String)
abstract fun setNumber(number: Int)
abstract fun setContentText(contentText: CharSequence?)
abstract fun setContentIntent(pendingIntent: PendingIntent?)
abstract fun setDeleteIntent(deleteIntent: PendingIntent?)
abstract fun setSortKey(sortKey: String)
abstract fun setOnlyAlertOnce(onlyAlertOnce: Boolean)
abstract fun addMessages(conversation: NotificationConversation)
abstract fun setGroupSummary(isGroupSummary: Boolean)
abstract fun setSubText(subText: String)
abstract fun addMarkAsReadActionActual(state: NotificationStateV2)
abstract fun setPriority(priority: Int)
abstract fun setAlarms(recipient: Recipient?)
abstract fun setTicker(ticker: CharSequence)
abstract fun addTurnOffJoinedNotificationsAction(pendingIntent: PendingIntent)
abstract fun setBubbleMetadata(conversation: NotificationConversation, bubbleState: BubbleUtil.BubbleState)
abstract fun setAutoCancel(autoCancel: Boolean)
abstract fun build(): Notification
protected abstract fun addPersonActual(recipient: Recipient)
protected abstract fun setWhen(timestamp: Long)
protected abstract fun addActions(replyMethod: ReplyMethod, conversation: NotificationConversation)
protected abstract fun addMessagesActual(state: NotificationStateV2)
protected abstract fun setLights(@ColorInt color: Int, onTime: Int, offTime: Int)
fun addPerson(recipient: Recipient) {
if (privacy.isDisplayContact) {
addPersonActual(recipient)
}
}
fun setWhen(conversation: NotificationConversation) {
if (conversation.getWhen() != 0L) {
setWhen(conversation.getWhen())
}
}
fun setWhen(notificationItem: NotificationItemV2) {
if (notificationItem.timestamp != 0L) {
setWhen(notificationItem.timestamp)
}
}
fun addReplyActions(conversation: NotificationConversation) {
if (!privacy.isDisplayMessage ||
KeyCachingService.isLocked(context) ||
!RecipientUtil.isMessageRequestAccepted(context, conversation.recipient)
) {
return
}
addActions(ReplyMethod.forRecipient(context, conversation.recipient), conversation)
}
fun addMarkAsReadAction(state: NotificationStateV2) {
if (privacy.isDisplayMessage) {
addMarkAsReadActionActual(state)
}
}
fun addMessages(state: NotificationStateV2) {
if (!privacy.isDisplayContact && !privacy.isDisplayMessage) {
return
}
addMessagesActual(state)
}
fun setSummaryContentText(recipient: Recipient) {
if (privacy.isDisplayContact) {
setContentText(context.getString(R.string.MessageNotifier_most_recent_from_s, recipient.getDisplayName(context)))
}
recipient.notificationChannel?.let { channel -> setChannelId(channel) }
}
fun setLights() {
val ledColor: String = TextSecurePreferences.getNotificationLedColor(context)
if (ledColor != "none") {
var blinkPattern = TextSecurePreferences.getNotificationLedPattern(context)
if (blinkPattern == "custom") {
blinkPattern = TextSecurePreferences.getNotificationLedPatternCustom(context)
}
val (onTime: Int, offTime: Int) = blinkPattern.parseBlinkPattern()
setLights(Color.parseColor(ledColor), onTime, offTime)
}
}
private fun String.parseBlinkPattern(): Pair<Int, Int> {
return split(",").let { parts -> parts[0].toInt() to parts[1].toInt() }
}
companion object {
fun create(context: Context): NotificationBuilder {
return if (Build.VERSION.SDK_INT >= 28) {
NotificationBuilderOS(context)
} else {
NotificationBuilderCompat(context)
}
}
}
/**
* Notification builder using solely androidx/compat libraries.
*/
class NotificationBuilderCompat(context: Context) : NotificationBuilder(context) {
val builder: NotificationCompat.Builder = NotificationCompat.Builder(context, NotificationChannels.getMessagesChannel(context))
override fun addActions(replyMethod: ReplyMethod, conversation: NotificationConversation) {
val markAsRead: PendingIntent = conversation.getMarkAsReadIntent(context)
val markAsReadAction: NotificationCompat.Action = NotificationCompat.Action.Builder(R.drawable.check, context.getString(R.string.MessageNotifier_mark_read), markAsRead)
.setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_MARK_AS_READ)
.build()
val extender: NotificationCompat.WearableExtender = NotificationCompat.WearableExtender()
builder.addAction(markAsReadAction)
extender.addAction(markAsReadAction)
if (conversation.mostRecentNotification.canReply(context)) {
val quickReply: PendingIntent = conversation.getQuickReplyIntent(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: 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())
.setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY)
.build()
} else {
NotificationCompat.Action(R.drawable.ic_reply_white_36dp, actionName, quickReply)
}
val wearableReplyAction = NotificationCompat.Action.Builder(R.drawable.ic_reply, actionName, remoteReply)
.addRemoteInput(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 = NotificationCompat.Action(R.drawable.check, context.getString(R.string.MessageNotifier_mark_all_as_read), state.getMarkAsReadIntent(context))
builder.addAction(markAllAsReadAction)
builder.extend(NotificationCompat.WearableExtender().addAction(markAllAsReadAction))
}
override fun addTurnOffJoinedNotificationsAction(pendingIntent: PendingIntent) {
val turnOffTheseNotifications = NotificationCompat.Action(
R.drawable.check,
context.getString(R.string.MessageNotifier_turn_off_these_notifications),
pendingIntent
)
builder.addAction(turnOffTheseNotifications)
}
override fun addMessages(conversation: NotificationConversation) {
val bigPictureUri: Uri? = conversation.getSlideBigPictureUri(context)
if (bigPictureUri != null) {
builder.setStyle(
NotificationCompat.BigPictureStyle()
.bigPicture(bigPictureUri.toBitmap(context, BIG_PICTURE_DIMEN))
.setSummaryText(conversation.getContentText(context))
.bigLargeIcon(null)
)
return
}
val messagingStyle: NotificationCompat.MessagingStyle = NotificationCompat.MessagingStyle(ConversationUtil.buildPersonCompat(context, Recipient.self()))
messagingStyle.conversationTitle = conversation.getConversationTitle(context)
messagingStyle.isGroupConversation = conversation.isGroup
conversation.notificationItems.forEach { notificationItem ->
val personBuilder: Person.Builder = Person.Builder()
.setKey(ConversationUtil.getShortcutId(notificationItem.individualRecipient))
.setBot(false)
.setName(notificationItem.getPersonName(context))
.setUri(notificationItem.getPersonUri(context))
.setIcon(notificationItem.getPersonIcon(context).toIconCompat())
val (dataUri: Uri?, mimeType: String?) = notificationItem.getThumbnailInfo()
messagingStyle.addMessage(NotificationCompat.MessagingStyle.Message(notificationItem.getPrimaryText(context), notificationItem.timestamp, personBuilder.build()).setData(mimeType, dataUri))
}
builder.setStyle(messagingStyle)
}
override fun addMessagesActual(state: NotificationStateV2) {
if (Build.VERSION.SDK_INT >= 24) {
return
}
val style: NotificationCompat.InboxStyle = NotificationCompat.InboxStyle()
for (notificationItem: NotificationItemV2 in state.notificationItems) {
val line: CharSequence? = notificationItem.getInboxLine(context)
if (line != null) {
style.addLine(line)
}
addPerson(notificationItem.individualRecipient)
}
builder.setStyle(style)
}
override fun setAlarms(recipient: Recipient?) {
if (NotificationChannels.supported()) {
return
}
val ringtone: Uri? = recipient?.messageRingtone
val vibrate = recipient?.messageVibrate
val defaultRingtone: Uri = TextSecurePreferences.getNotificationRingtone(context)
val defaultVibrate: Boolean = TextSecurePreferences.isNotificationVibrateEnabled(context)
if (ringtone == null && !TextUtils.isEmpty(defaultRingtone.toString())) {
builder.setSound(defaultRingtone)
} else if (ringtone != null && ringtone.toString().isNotEmpty()) {
builder.setSound(ringtone)
}
if (vibrate == RecipientDatabase.VibrateState.ENABLED || vibrate == RecipientDatabase.VibrateState.DEFAULT && defaultVibrate) {
builder.setDefaults(Notification.DEFAULT_VIBRATE)
}
}
override fun setBubbleMetadata(conversation: NotificationConversation, bubbleState: BubbleUtil.BubbleState) {
// Intentionally left blank
}
override fun setLights(@ColorInt color: Int, onTime: Int, offTime: Int) {
builder.setLights(color, onTime, offTime)
}
override fun setSmallIcon(drawable: Int) {
builder.setSmallIcon(drawable)
}
override fun setColor(@ColorInt color: Int) {
builder.color = 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 setShortcutId(shortcutId: String) {
builder.setShortcutId(shortcutId)
}
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) {
if (!NotificationChannels.supported()) {
builder.priority = priority
}
}
override fun setAutoCancel(autoCancel: Boolean) {
builder.setAutoCancel(autoCancel)
}
override fun build(): Notification {
return builder.build()
}
override fun addPersonActual(recipient: Recipient) {
builder.addPerson(recipient.contactUri.toString())
}
override fun setWhen(timestamp: Long) {
builder.setWhen(timestamp)
}
override fun setGroupSummary(isGroupSummary: Boolean) {
builder.setGroupSummary(isGroupSummary)
}
override fun setSubText(subText: String) {
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 addMessages(conversation: NotificationConversation) {
val bigPictureUri: Uri? = conversation.getSlideBigPictureUri(context)
if (bigPictureUri != null) {
builder.style = Notification.BigPictureStyle()
.bigPicture(bigPictureUri.toBitmap(context, BIG_PICTURE_DIMEN))
.setSummaryText(conversation.getContentText(context))
.bigLargeIcon(null as Bitmap?)
return
}
val messagingStyle: Notification.MessagingStyle = Notification.MessagingStyle(ConversationUtil.buildPerson(context, Recipient.self()))
messagingStyle.conversationTitle = conversation.getConversationTitle(context)
messagingStyle.isGroupConversation = conversation.isGroup
conversation.notificationItems.forEach { notificationItem ->
val personBuilder: android.app.Person.Builder = android.app.Person.Builder()
.setKey(ConversationUtil.getShortcutId(notificationItem.individualRecipient))
.setBot(false)
.setName(notificationItem.getPersonName(context))
.setUri(notificationItem.getPersonUri(context))
.setIcon(notificationItem.getPersonIcon(context).toIcon())
val (dataUri: Uri?, mimeType: String?) = notificationItem.getThumbnailInfo()
messagingStyle.addMessage(Notification.MessagingStyle.Message(notificationItem.getPrimaryText(context), notificationItem.timestamp, personBuilder.build()).setData(mimeType, dataUri))
}
builder.style = messagingStyle
}
override fun setBubbleMetadata(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 setShortcutId(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)
}
override fun setGroupSummary(isGroupSummary: Boolean) {
builder.setGroupSummary(isGroupSummary)
}
override fun setSubText(subText: String) {
builder.setSubText(subText)
}
}
}
private fun Bitmap?.toIconCompat(): IconCompat? {
return if (this != null) {
IconCompat.createWithBitmap(this)
} else {
null
}
}
@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) {
ReplyMethod.GroupMessage -> R.string.MessageNotifier_reply
ReplyMethod.SecureMessage -> R.string.MessageNotifier_signal_message
ReplyMethod.UnsecuredSmsMessage -> R.string.MessageNotifier_unsecured_sms
}
}

Wyświetl plik

@ -0,0 +1,191 @@
package org.thoughtcrime.securesms.notifications.v2
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.net.Uri
import android.text.SpannableStringBuilder
import androidx.core.app.TaskStackBuilder
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.contacts.TurnOffContactJoinedNotificationsActivity
import org.thoughtcrime.securesms.contacts.avatars.ContactColors
import org.thoughtcrime.securesms.contacts.avatars.GeneratedContactPhoto
import org.thoughtcrime.securesms.conversation.ConversationIntents
import org.thoughtcrime.securesms.notifications.DeleteNotificationReceiver
import org.thoughtcrime.securesms.notifications.MarkReadReceiver
import org.thoughtcrime.securesms.notifications.NotificationIds
import org.thoughtcrime.securesms.notifications.RemoteReplyReceiver
import org.thoughtcrime.securesms.notifications.ReplyMethod
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
private const val LARGE_ICON_DIMEN = 250
/**
* Encapsulate all the notifications for a given conversation (thread) and the top
* level information about said conversation.
*/
class NotificationConversation(
val recipient: Recipient,
val threadId: Long,
unsortedNotificationItems: List<NotificationItemV2>
) {
val notificationItems: List<NotificationItemV2> = unsortedNotificationItems.sorted()
val mostRecentNotification: NotificationItemV2 = notificationItems.last()
val notificationId: Int = NotificationIds.getNotificationIdForThread(threadId)
val sortKey: Long = Long.MAX_VALUE - mostRecentNotification.timestamp
val messageCount: Int = notificationItems.size
val isGroup: Boolean = recipient.isGroup
fun getContentTitle(context: Context): CharSequence {
return if (TextSecurePreferences.getNotificationPrivacy(context).isDisplayContact) {
recipient.getDisplayName(context)
} else {
context.getString(R.string.SingleRecipientNotificationBuilder_signal)
}
}
fun getLargeIcon(context: Context): Bitmap? {
if (TextSecurePreferences.getNotificationPrivacy(context).isDisplayMessage) {
val largeIconUri: Uri? = getSlideLargeIcon()
if (largeIconUri != null) {
return largeIconUri.toBitmap(context, LARGE_ICON_DIMEN)
}
}
return getContactLargeIcon(context).toLargeBitmap(context)
}
private fun getContactLargeIcon(context: Context): Drawable? {
return if (TextSecurePreferences.getNotificationPrivacy(context).isDisplayContact) {
recipient.getContactDrawable(context)
} else {
GeneratedContactPhoto("Unknown", R.drawable.ic_profile_outline_40).asDrawable(context, ContactColors.UNKNOWN_COLOR.toConversationColor(context))
}
}
fun getContactUri(context: Context): String? {
return if (TextSecurePreferences.getNotificationPrivacy(context).isDisplayContact) {
recipient.contactUri?.toString()
} else {
null
}
}
private fun getSlideLargeIcon(): Uri? {
return if (notificationItems.size == 1) mostRecentNotification.getLargeIconUri() else null
}
fun getSlideBigPictureUri(context: Context): Uri? {
return if (notificationItems.size == 1 && TextSecurePreferences.getNotificationPrivacy(context).isDisplayMessage) mostRecentNotification.getBigPictureUri() else null
}
fun getContentText(context: Context): CharSequence? {
val privacy: NotificationPrivacyPreference = TextSecurePreferences.getNotificationPrivacy(context)
val stringBuilder = SpannableStringBuilder()
if (privacy.isDisplayContact && recipient.isGroup) {
stringBuilder.append(Util.getBoldedString(mostRecentNotification.individualRecipient.getDisplayName(context) + ": "))
}
return if (privacy.isDisplayMessage) {
stringBuilder.append(mostRecentNotification.getPrimaryText(context))
} else {
stringBuilder.append(context.getString(R.string.SingleRecipientNotificationBuilder_new_message))
}
}
fun getConversationTitle(context: Context): CharSequence? {
if (isGroup) {
return if (TextSecurePreferences.getNotificationPrivacy(context).isDisplayContact) {
recipient.getDisplayName(context)
} else {
context.getString(R.string.SingleRecipientNotificationBuilder_signal)
}
}
return null
}
fun getWhen(): Long {
return mostRecentNotification.timestamp
}
fun hasNewNotifications(): Boolean {
return notificationItems.any { it.isNewNotification }
}
fun getPendingIntent(context: Context): PendingIntent {
val intent: Intent = ConversationIntents.createBuilder(context, recipient.id, threadId)
.withStartingPosition(mostRecentNotification.getStartingPosition(context))
.build()
.makeUniqueToPreventMerging()
return TaskStackBuilder.create(context)
.addNextIntentWithParentStack(intent)
.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)!!
}
fun getDeleteIntent(context: Context): PendingIntent? {
var index = 0
val ids = LongArray(notificationItems.size)
val mms = BooleanArray(ids.size)
notificationItems.forEach { notificationItem ->
ids[index] = notificationItem.id
mms[index++] = notificationItem.isMms
}
val intent = Intent(context, DeleteNotificationReceiver::class.java)
.setAction(DeleteNotificationReceiver.DELETE_NOTIFICATION_ACTION)
.putExtra(DeleteNotificationReceiver.EXTRA_IDS, ids)
.putExtra(DeleteNotificationReceiver.EXTRA_MMS, mms)
.makeUniqueToPreventMerging()
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
}
fun getMarkAsReadIntent(context: Context): PendingIntent {
val intent = Intent(context, MarkReadReceiver::class.java).setAction(MarkReadReceiver.CLEAR_ACTION)
.putExtra(MarkReadReceiver.THREAD_IDS_EXTRA, longArrayOf(mostRecentNotification.threadId))
.putExtra(MarkReadReceiver.NOTIFICATION_ID_EXTRA, notificationId)
.makeUniqueToPreventMerging()
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
}
fun getQuickReplyIntent(context: Context): PendingIntent {
val intent: Intent = ConversationIntents.createPopUpBuilder(context, recipient.id, mostRecentNotification.threadId)
.build()
.makeUniqueToPreventMerging()
return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
}
fun getRemoteReplyIntent(context: Context, replyMethod: ReplyMethod): PendingIntent {
val intent = Intent(context, RemoteReplyReceiver::class.java)
.setAction(RemoteReplyReceiver.REPLY_ACTION)
.putExtra(RemoteReplyReceiver.RECIPIENT_EXTRA, recipient.id)
.putExtra(RemoteReplyReceiver.REPLY_METHOD, replyMethod)
.setPackage(context.packageName)
.makeUniqueToPreventMerging()
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
}
fun getTurnOffJoinedNotificationsIntent(context: Context): PendingIntent {
return PendingIntent.getActivity(
context,
0,
TurnOffContactJoinedNotificationsActivity.newIntent(context, threadId),
PendingIntent.FLAG_UPDATE_CURRENT
)
}
override fun toString(): String {
return "NotificationConversation(threadId=$threadId, notificationItems=$notificationItems, messageCount=$messageCount, hasNewNotifications=${hasNewNotifications()})"
}
}

Wyświetl plik

@ -0,0 +1,69 @@
package org.thoughtcrime.securesms.notifications.v2
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.net.Uri
import com.bumptech.glide.load.engine.DiskCacheStrategy
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto
import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri
import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.BitmapUtil
import java.util.concurrent.ExecutionException
fun Drawable?.toLargeBitmap(context: Context): Bitmap? {
if (this == null) {
return null
}
val largeIconTargetSize: Int = context.resources.getDimensionPixelSize(R.dimen.contact_photo_target_size)
return BitmapUtil.createFromDrawable(this, largeIconTargetSize, largeIconTargetSize)
}
fun Recipient.getContactDrawable(context: Context): Drawable? {
val contactPhoto: ContactPhoto? = contactPhoto
val fallbackContactPhoto: FallbackContactPhoto = fallbackContactPhoto
return if (contactPhoto != null) {
try {
GlideApp.with(context.applicationContext)
.load(contactPhoto)
.diskCacheStrategy(DiskCacheStrategy.ALL)
.circleCrop()
.submit(
context.resources.getDimensionPixelSize(android.R.dimen.notification_large_icon_width),
context.resources.getDimensionPixelSize(android.R.dimen.notification_large_icon_height)
)
.get()
} catch (e: InterruptedException) {
fallbackContactPhoto.asDrawable(context, color.toConversationColor(context))
} catch (e: ExecutionException) {
fallbackContactPhoto.asDrawable(context, color.toConversationColor(context))
}
} else {
fallbackContactPhoto.asDrawable(context, color.toConversationColor(context))
}
}
fun Uri.toBitmap(context: Context, dimension: Int): Bitmap {
return try {
GlideApp.with(context.applicationContext)
.asBitmap()
.load(DecryptableUri(this))
.diskCacheStrategy(DiskCacheStrategy.NONE)
.submit(dimension, dimension)
.get()
} catch (e: InterruptedException) {
Bitmap.createBitmap(dimension, dimension, Bitmap.Config.RGB_565)
} catch (e: ExecutionException) {
Bitmap.createBitmap(dimension, dimension, Bitmap.Config.RGB_565)
}
}
fun Intent.makeUniqueToPreventMerging(): Intent {
return setData((Uri.parse("custom://" + System.currentTimeMillis())))
}

Wyświetl plik

@ -0,0 +1,254 @@
package org.thoughtcrime.securesms.notifications.v2
import android.app.Notification
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.graphics.BitmapFactory
import android.media.AudioAttributes
import android.media.AudioManager
import android.media.RingtoneManager
import android.net.Uri
import android.os.Build
import android.os.TransactionTooLargeException
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import org.signal.core.util.concurrent.SignalExecutors
import org.signal.core.util.logging.Log
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.notifications.DefaultMessageNotifier
import org.thoughtcrime.securesms.notifications.NotificationChannels
import org.thoughtcrime.securesms.notifications.NotificationIds
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.BubbleUtil
import org.thoughtcrime.securesms.util.ConversationUtil
import org.thoughtcrime.securesms.util.FeatureFlags
import org.thoughtcrime.securesms.util.ServiceUtil
import org.thoughtcrime.securesms.util.TextSecurePreferences
private val TAG = Log.tag(NotificationFactory::class.java)
/**
* Given a notification state consisting of conversations of messages, show appropriate system notifications.
*/
object NotificationFactory {
fun notify(
context: Context,
state: NotificationStateV2,
visibleThreadId: Long,
targetThreadId: Long,
defaultBubbleState: BubbleUtil.BubbleState,
lastAudibleNotification: Long,
alertOverrides: Set<Long>
): Set<Long> {
if (state.isEmpty) {
Log.d(TAG, "State is empty, bailing")
return emptySet()
}
val threadsThatNewlyAlerted: MutableSet<Long> = mutableSetOf()
if (Build.VERSION.SDK_INT >= 23 || state.conversations.size == 1) {
state.conversations.forEach { conversation ->
if (conversation.threadId == visibleThreadId && conversation.hasNewNotifications()) {
notifyInThread(context, conversation.recipient, lastAudibleNotification)
} else if (conversation.hasNewNotifications() || alertOverrides.contains(conversation.threadId)) {
if (conversation.hasNewNotifications()) {
threadsThatNewlyAlerted += conversation.threadId
}
notifyForConversation(
context = context,
conversation = conversation,
recipient = conversation.recipient,
targetThreadId = targetThreadId,
defaultBubbleState = defaultBubbleState
)
}
}
}
if (state.conversations.size > 1 || ServiceUtil.getNotificationManager(context).isDisplayingSummaryNotification()) {
val builder: NotificationBuilder = NotificationBuilder.create(context)
builder.apply {
setSmallIcon(R.drawable.ic_notification)
setColor(ContextCompat.getColor(context, R.color.core_ultramarine))
setCategory(NotificationCompat.CATEGORY_MESSAGE)
setGroup(DefaultMessageNotifier.NOTIFICATION_GROUP)
setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN)
setChannelId(NotificationChannels.getMessagesChannel(context))
setContentTitle(context.getString(R.string.app_name))
setContentIntent(PendingIntent.getActivity(context, 0, MainActivity.clearTop(context), 0))
setGroupSummary(true)
setSubText(context.getString(R.string.MessageNotifier_d_new_messages_in_d_conversations, state.messageCount, state.threadCount))
setContentInfo(state.messageCount.toString())
setNumber(state.messageCount)
setSummaryContentText(state.mostRecentSender)
setDeleteIntent(state.getDeleteIntent(context))
setWhen(state.mostRecentNotification)
addMarkAsReadAction(state)
addMessages(state)
setOnlyAlertOnce(!state.notificationItems.any { it.isNewNotification })
setPriority(TextSecurePreferences.getNotificationPriority(context))
setLights()
setAlarms(state.mostRecentSender)
setTicker(state.mostRecentNotification.getStyledPrimaryText(context, true))
}
Log.d(TAG, "showing summary notification")
NotificationManagerCompat.from(context).safelyNotify(context, null, NotificationIds.MESSAGE_SUMMARY, builder.build())
}
return threadsThatNewlyAlerted
}
private fun notifyForConversation(
context: Context,
conversation: NotificationConversation,
recipient: Recipient,
targetThreadId: Long,
defaultBubbleState: BubbleUtil.BubbleState
) {
val builder: NotificationBuilder = NotificationBuilder.create(context)
builder.apply {
setSmallIcon(R.drawable.ic_notification)
setColor(ContextCompat.getColor(context, R.color.core_ultramarine))
setCategory(NotificationCompat.CATEGORY_MESSAGE)
setGroup(DefaultMessageNotifier.NOTIFICATION_GROUP)
setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN)
setChannelId(recipient.notificationChannel ?: NotificationChannels.getMessagesChannel(context))
setContentTitle(conversation.getContentTitle(context))
setLargeIcon(conversation.getLargeIcon(context))
addPerson(recipient)
setShortcutId(ConversationUtil.getShortcutId(recipient))
setContentInfo(conversation.messageCount.toString())
setNumber(conversation.messageCount)
setContentText(conversation.getContentText(context))
setContentIntent(conversation.getPendingIntent(context))
setDeleteIntent(conversation.getDeleteIntent(context))
setSortKey(conversation.sortKey.toString())
setWhen(conversation)
addReplyActions(conversation)
setOnlyAlertOnce(false)
addMessages(conversation)
setPriority(TextSecurePreferences.getNotificationPriority(context))
setLights()
setAlarms(conversation.recipient)
setTicker(conversation.mostRecentNotification.getStyledPrimaryText(context, true))
setBubbleMetadata(conversation, if (targetThreadId == conversation.threadId) defaultBubbleState else BubbleUtil.BubbleState.HIDDEN)
}
if (conversation.messageCount == 1 && conversation.mostRecentNotification.isJoined) {
builder.addTurnOffJoinedNotificationsAction(conversation.getTurnOffJoinedNotificationsIntent(context))
}
NotificationManagerCompat.from(context).safelyNotify(context, conversation.recipient, conversation.notificationId, builder.build())
}
private fun notifyInThread(context: Context, recipient: Recipient, lastAudibleNotification: Long) {
if (!TextSecurePreferences.isInThreadNotifications(context) ||
ServiceUtil.getAudioManager(context).ringerMode != AudioManager.RINGER_MODE_NORMAL ||
(System.currentTimeMillis() - lastAudibleNotification) < DefaultMessageNotifier.MIN_AUDIBLE_PERIOD_MILLIS
) {
return
}
val uri: Uri = if (NotificationChannels.supported()) {
NotificationChannels.getMessageRingtone(context, recipient) ?: NotificationChannels.getMessageRingtone(context)
} else {
recipient.messageRingtone ?: TextSecurePreferences.getNotificationRingtone(context)
}
if (uri.toString().isEmpty()) {
Log.d(TAG, "ringtone uri is empty")
return
}
val ringtone = RingtoneManager.getRingtone(context, uri)
if (ringtone == null) {
Log.w(TAG, "ringtone is null")
return
}
if (Build.VERSION.SDK_INT >= 21) {
ringtone.audioAttributes = AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_UNKNOWN)
.setUsage(AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT)
.build()
} else {
@Suppress("DEPRECATION")
ringtone.streamType = AudioManager.STREAM_NOTIFICATION
}
ringtone.play()
}
fun notifyMessageDeliveryFailed(context: Context, recipient: Recipient, threadId: Long, visibleThread: Long) {
if (threadId == visibleThread) {
notifyInThread(context, recipient, 0)
return
}
val intent: Intent = ConversationIntents.createBuilder(context, recipient.id, threadId)
.build()
.makeUniqueToPreventMerging()
val builder: NotificationBuilder = NotificationBuilder.create(context)
builder.apply {
setSmallIcon(R.drawable.ic_notification)
setLargeIcon(BitmapFactory.decodeResource(context.resources, 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(recipient)
setChannelId(NotificationChannels.FAILURES)
}
NotificationManagerCompat.from(context).safelyNotify(context, recipient, threadId.toInt(), builder.build())
}
private fun NotificationManager.isDisplayingSummaryNotification(): Boolean {
if (Build.VERSION.SDK_INT >= 23) {
try {
return activeNotifications.any { notification -> notification.id == NotificationIds.MESSAGE_SUMMARY }
} catch (e: Throwable) {
}
}
return false
}
private fun NotificationManagerCompat.safelyNotify(context: Context, threadRecipient: Recipient?, notificationId: Int, notification: Notification) {
try {
notify(notificationId, notification)
if (FeatureFlags.internalUser()) {
Log.i(TAG, "Posted notification: $notification")
}
} catch (e: SecurityException) {
Log.i(TAG, "Security exception when posting notification, clearing ringtone")
if (threadRecipient != null) {
SignalExecutors.BOUNDED.execute {
DatabaseFactory.getRecipientDatabase(context).setMessageRingtone(threadRecipient.id, null)
NotificationChannels.updateMessageRingtone(context, threadRecipient, null)
}
}
} catch (runtimeException: RuntimeException) {
if (runtimeException.cause is TransactionTooLargeException) {
Log.e(TAG, "Transaction too large", runtimeException)
} else {
throw runtimeException
}
}
}
}

Wyświetl plik

@ -0,0 +1,287 @@
package org.thoughtcrime.securesms.notifications.v2
import android.content.Context
import android.graphics.Bitmap
import android.net.Uri
import android.text.SpannableStringBuilder
import android.text.TextUtils
import androidx.annotation.StringRes
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.database.DatabaseFactory
import org.thoughtcrime.securesms.database.MentionUtil
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.mms.Slide
import org.thoughtcrime.securesms.mms.SlideDeck
import org.thoughtcrime.securesms.notifications.AbstractNotificationBuilder
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.service.KeyCachingService
import org.thoughtcrime.securesms.util.MediaUtil
import org.thoughtcrime.securesms.util.MessageRecordUtil
import org.thoughtcrime.securesms.util.SpanUtil
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.thoughtcrime.securesms.util.Util
private val TAG: String = Log.tag(NotificationItemV2::class.java)
private const val EMOJI_REPLACEMENT_STRING = "__EMOJI__"
/**
* Base for messaged-based notifications. Represents a single notification.
*/
sealed class NotificationItemV2(val threadRecipient: Recipient, protected val record: MessageRecord) : Comparable<NotificationItemV2> {
val id: Long = record.id
val threadId: Long = record.threadId
val isMms: Boolean = record.isMms
val slideDeck: SlideDeck? = (record as? MmsMessageRecord)?.slideDeck
val isJoined: Boolean = record.isJoined
protected val notifiedTimestamp: Long = record.notifiedTimestamp
abstract val timestamp: Long
abstract val individualRecipient: Recipient
abstract val isNewNotification: Boolean
protected abstract fun getPrimaryTextActual(context: Context): CharSequence
abstract fun getStartingPosition(context: Context): Int
abstract fun getLargeIconUri(): Uri?
abstract fun getBigPictureUri(): Uri?
abstract fun canReply(context: Context): Boolean
protected fun getMessageContentType(messageRecord: MmsMessageRecord): String {
val thumbnailSlide: Slide? = messageRecord.slideDeck.thumbnailSlide
return if (thumbnailSlide == null) {
val slideContentType: String? = messageRecord.slideDeck.firstSlideContentType
if (slideContentType != null) {
slideContentType
} else {
Log.w(TAG, "Could not distinguish view-once content type from message record, defaulting to JPEG")
MediaUtil.IMAGE_JPEG
}
} else {
thumbnailSlide.contentType
}
}
fun getStyledPrimaryText(context: Context, trimmed: Boolean = false): CharSequence {
return if (TextSecurePreferences.getNotificationPrivacy(context).isDisplayNothing) {
context.getString(R.string.SingleRecipientNotificationBuilder_new_message)
} else {
SpannableStringBuilder().apply {
append(Util.getBoldedString(individualRecipient.getShortDisplayNameIncludingUsername(context)))
if (threadRecipient != individualRecipient) {
append(Util.getBoldedString("@${threadRecipient.getDisplayName(context)}"))
}
append(": ")
append(getPrimaryText(context).apply { if (trimmed) trimToDisplayLength() })
}
}
}
fun getPersonName(context: Context): CharSequence {
return if (TextSecurePreferences.getNotificationPrivacy(context).isDisplayContact) {
individualRecipient.getDisplayName(context)
} else {
""
}
}
override fun compareTo(other: NotificationItemV2): Int {
return timestamp.compareTo(other.timestamp)
}
fun getPersonUri(context: Context): String? {
return if (TextSecurePreferences.getNotificationPrivacy(context).isDisplayContact && individualRecipient.isSystemContact) {
individualRecipient.contactUri.toString()
} else {
null
}
}
fun getPersonIcon(context: Context): Bitmap? {
return if (TextSecurePreferences.getNotificationPrivacy(context).isDisplayContact) {
individualRecipient.getContactDrawable(context).toLargeBitmap(context)
} else {
null
}
}
fun getPrimaryText(context: Context): CharSequence {
return if (TextSecurePreferences.getNotificationPrivacy(context).isDisplayMessage) {
getPrimaryTextActual(context)
} else {
context.getString(R.string.SingleRecipientNotificationBuilder_new_message)
}
}
fun getThumbnailInfo(): ThumbnailInfo {
val thumbnailSlide: Slide? = slideDeck?.thumbnailSlide
return ThumbnailInfo(thumbnailSlide?.publicUri, thumbnailSlide?.contentType)
}
fun getInboxLine(context: Context): CharSequence? {
return when {
TextSecurePreferences.getNotificationPrivacy(context).isDisplayNothing -> null
else -> getStyledPrimaryText(context, true)
}
}
private fun CharSequence?.trimToDisplayLength(): CharSequence {
val text: CharSequence = this ?: ""
return if (text.length <= AbstractNotificationBuilder.MAX_DISPLAY_LENGTH) {
text
} else {
text.subSequence(0, AbstractNotificationBuilder.MAX_DISPLAY_LENGTH)
}
}
data class ThumbnailInfo(val uri: Uri?, val contentType: String?)
}
/**
* Represents a notification associated with a new message.
*/
class MessageNotification(threadRecipient: Recipient, record: MessageRecord) : NotificationItemV2(threadRecipient, record) {
override val timestamp: Long = record.timestamp
override val individualRecipient: Recipient = record.individualRecipient.resolve()
override val isNewNotification: Boolean = notifiedTimestamp == 0L
override fun getPrimaryTextActual(context: Context): CharSequence {
return if (KeyCachingService.isLocked(context)) {
SpanUtil.italic(context.getString(R.string.MessageNotifier_locked_message))
} else if (record.isMms && (record as MmsMessageRecord).sharedContacts.isNotEmpty()) {
val contact = record.sharedContacts[0]
ContactUtil.getStringSummary(context, contact)
} else if (record.isMms && record.isViewOnce) {
SpanUtil.italic(context.getString(getViewOnceDescription(record as MmsMessageRecord)))
} else if (record.isRemoteDelete) {
SpanUtil.italic(context.getString(R.string.MessageNotifier_this_message_was_deleted))
} else if (record.isMms && !record.isMmsNotification && (record as MmsMessageRecord).slideDeck.slides.isNotEmpty()) {
ThreadBodyUtil.getFormattedBodyFor(context, record)
} else if (record.isGroupCall) {
MessageRecord.getGroupCallUpdateDescription(context, record.body, false).string
} else {
MentionUtil.updateBodyWithDisplayNames(context, record)
}
}
@StringRes
private fun getViewOnceDescription(messageRecord: MmsMessageRecord): Int {
val contentType = getMessageContentType(messageRecord)
return if (MediaUtil.isImageType(contentType)) R.string.MessageNotifier_view_once_photo else R.string.MessageNotifier_view_once_video
}
override fun getStartingPosition(context: Context): Int {
return -1
}
override fun getLargeIconUri(): Uri? {
val slide: Slide? = slideDeck?.thumbnailSlide ?: slideDeck?.stickerSlide
return if (slide?.isInProgress == false) slide.uri else null
}
override fun getBigPictureUri(): Uri? {
val slide: Slide? = slideDeck?.thumbnailSlide
return if (slide?.isInProgress == false) slide.uri else null
}
override fun canReply(context: Context): Boolean {
if (KeyCachingService.isLocked(context) ||
record.isRemoteDelete ||
record.isGroupCall ||
record.isViewOnce ||
record.isJoined
) {
return false
}
if (record is MmsMessageRecord) {
return (record.isMmsNotification || record.slideDeck.slides.isEmpty()) && record.sharedContacts.isEmpty()
}
return true
}
override fun toString(): String {
return "MessageNotification(timestamp=$timestamp, isNewNotification=$isNewNotification)"
}
}
/**
* Represents a notification associated with a new reaction.
*/
class ReactionNotification(threadRecipient: Recipient, record: MessageRecord, val reaction: ReactionRecord) : NotificationItemV2(threadRecipient, record) {
override val timestamp: Long = reaction.dateReceived
override val individualRecipient: Recipient = Recipient.resolved(reaction.author)
override val isNewNotification: Boolean = timestamp > notifiedTimestamp
override fun getPrimaryTextActual(context: Context): CharSequence {
return if (KeyCachingService.isLocked(context)) {
SpanUtil.italic(context.getString(R.string.MessageNotifier_locked_message))
} else {
val text: String = SpanUtil.italic(getReactionMessageBody(context)).toString()
val parts: Array<String> = text.split(EMOJI_REPLACEMENT_STRING).toTypedArray()
val builder = SpannableStringBuilder()
parts.forEachIndexed { i, part ->
builder.append(SpanUtil.italic(part))
if (i != parts.size - 1) {
builder.append(reaction.emoji)
}
}
if (text.endsWith(EMOJI_REPLACEMENT_STRING)) {
builder.append(reaction.emoji)
}
builder
}
}
private fun getReactionMessageBody(context: Context): CharSequence {
val body: CharSequence = MentionUtil.updateBodyWithDisplayNames(context, record)
val bodyIsEmpty: Boolean = TextUtils.isEmpty(body)
return if (MessageRecordUtil.hasSharedContact(record)) {
val contact: Contact = (record as MmsMessageRecord).sharedContacts[0]
val summary: CharSequence = ContactUtil.getStringSummary(context, contact)
context.getString(R.string.MessageNotifier_reacted_s_to_s, EMOJI_REPLACEMENT_STRING, summary)
} else if (MessageRecordUtil.hasSticker(record)) {
context.getString(R.string.MessageNotifier_reacted_s_to_your_sticker, EMOJI_REPLACEMENT_STRING)
} else if (record.isMms && record.isViewOnce) {
context.getString(R.string.MessageNotifier_reacted_s_to_your_view_once_media, EMOJI_REPLACEMENT_STRING)
} else if (!bodyIsEmpty) {
context.getString(R.string.MessageNotifier_reacted_s_to_s, EMOJI_REPLACEMENT_STRING, body)
} else if (MessageRecordUtil.isMediaMessage(record) && MediaUtil.isVideoType(getMessageContentType((record as MmsMessageRecord)))) {
context.getString(R.string.MessageNotifier_reacted_s_to_your_video, EMOJI_REPLACEMENT_STRING)
} else if (MessageRecordUtil.isMediaMessage(record) && MediaUtil.isImageType(getMessageContentType((record as MmsMessageRecord)))) {
context.getString(R.string.MessageNotifier_reacted_s_to_your_image, EMOJI_REPLACEMENT_STRING)
} else if (MessageRecordUtil.isMediaMessage(record) && MediaUtil.isAudioType(getMessageContentType((record as MmsMessageRecord)))) {
context.getString(R.string.MessageNotifier_reacted_s_to_your_audio, EMOJI_REPLACEMENT_STRING)
} else if (MessageRecordUtil.isMediaMessage(record)) {
context.getString(R.string.MessageNotifier_reacted_s_to_your_file, EMOJI_REPLACEMENT_STRING)
} else {
context.getString(R.string.MessageNotifier_reacted_s_to_s, EMOJI_REPLACEMENT_STRING, body)
}
}
override fun getStartingPosition(context: Context): Int {
return DatabaseFactory.getMmsSmsDatabase(context).getMessagePositionInConversation(threadId, record.dateReceived)
}
override fun getLargeIconUri(): Uri? = null
override fun getBigPictureUri(): Uri? = null
override fun canReply(context: Context): Boolean = false
override fun toString(): String {
return "ReactionNotification(timestamp=$timestamp, isNewNotification=$isNewNotification)"
}
}

Wyświetl plik

@ -0,0 +1,92 @@
package org.thoughtcrime.securesms.notifications.v2
import android.content.Context
import androidx.annotation.WorkerThread
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.database.MmsSmsColumns
import org.thoughtcrime.securesms.database.MmsSmsDatabase
import org.thoughtcrime.securesms.database.RecipientDatabase
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.ReactionRecord
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.CursorUtil
/**
* Queries the message databases to determine messages that should be in notifications.
*/
object NotificationStateProvider {
@WorkerThread
fun constructNotificationState(context: Context): NotificationStateV2 {
val messages: MutableList<NotificationMessage> = mutableListOf()
DatabaseFactory.getMmsSmsDatabase(context).unread.use { unreadMessages ->
if (unreadMessages.count == 0) {
return NotificationStateV2.EMPTY
}
MmsSmsDatabase.readerFor(unreadMessages).use { reader ->
var record: MessageRecord? = reader.next
while (record != null) {
messages += NotificationMessage(
messageRecord = record,
threadRecipient = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(record.threadId)?.resolve() ?: Recipient.UNKNOWN,
threadId = record.threadId,
isUnreadMessage = CursorUtil.requireInt(unreadMessages, MmsSmsColumns.READ) == 0,
hasUnreadReactions = CursorUtil.requireInt(unreadMessages, MmsSmsColumns.REACTIONS_UNREAD) == 1,
lastReactionRead = CursorUtil.requireLong(unreadMessages, MmsSmsColumns.REACTIONS_LAST_SEEN)
)
record = reader.next
}
}
}
val conversations: MutableList<NotificationConversation> = mutableListOf()
messages.groupBy { it.threadId }
.forEach { (threadId, threadMessages) ->
val notificationItems: MutableList<NotificationItemV2> = mutableListOf()
for (notification: NotificationMessage in threadMessages) {
if (notification.includeMessage()) {
notificationItems += MessageNotification(notification.threadRecipient, notification.messageRecord)
}
if (notification.hasUnreadReactions) {
notification.messageRecord.reactions.filter { notification.includeReaction(it) }
.forEach { notificationItems += ReactionNotification(notification.threadRecipient, notification.messageRecord, it) }
}
}
if (notificationItems.isNotEmpty()) {
conversations += NotificationConversation(notificationItems[0].threadRecipient, threadId, notificationItems)
}
}
return NotificationStateV2(conversations)
}
private data class NotificationMessage(
val messageRecord: MessageRecord,
val threadRecipient: Recipient,
val threadId: Long,
val isUnreadMessage: Boolean,
val hasUnreadReactions: Boolean,
val lastReactionRead: Long
) {
private val unknownOrNotMutedThread: Boolean = threadRecipient == Recipient.UNKNOWN || threadRecipient.isNotMuted
fun includeMessage(): Boolean {
return isUnreadMessage && (unknownOrNotMutedThread || (threadRecipient.isAlwaysNotifyMentions && messageRecord.hasSelfMention()))
}
fun includeReaction(reaction: ReactionRecord): Boolean {
return reaction.author != Recipient.self().id && messageRecord.isOutgoing && reaction.dateReceived > lastReactionRead && unknownOrNotMutedThread
}
private val Recipient.isNotMuted: Boolean
get() = !isMuted
private val Recipient.isAlwaysNotifyMentions: Boolean
get() = mentionSetting == RecipientDatabase.MentionSetting.ALWAYS_NOTIFY
}
}

Wyświetl plik

@ -0,0 +1,80 @@
package org.thoughtcrime.securesms.notifications.v2
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import org.thoughtcrime.securesms.notifications.DeleteNotificationReceiver
import org.thoughtcrime.securesms.notifications.MarkReadReceiver
import org.thoughtcrime.securesms.notifications.NotificationIds
import org.thoughtcrime.securesms.recipients.Recipient
/**
* Hold all state for notifications for all conversations.
*/
data class NotificationStateV2(val conversations: List<NotificationConversation>) {
val threadCount: Int = conversations.size
val isEmpty: Boolean = conversations.isEmpty()
val messageCount: Int by lazy {
conversations.fold(0) { messageCount, conversation ->
messageCount + conversation.messageCount
}
}
val notificationItems: List<NotificationItemV2> by lazy {
conversations.map { it.notificationItems }
.flatten()
.sorted()
}
val notificationIds: Set<Int> by lazy {
conversations.map { it.notificationId }
.toSet()
}
val mostRecentNotification: NotificationItemV2
get() = notificationItems.last()
val mostRecentSender: Recipient
get() = mostRecentNotification.individualRecipient
fun getDeleteIntent(context: Context): PendingIntent? {
val ids = LongArray(messageCount)
val mms = BooleanArray(ids.size)
conversations.forEach { conversation ->
conversation.notificationItems.forEachIndexed { index, notificationItem ->
ids[index] = notificationItem.id
mms[index] = notificationItem.isMms
}
}
val intent = Intent(context, DeleteNotificationReceiver::class.java)
.setAction(DeleteNotificationReceiver.DELETE_NOTIFICATION_ACTION)
.putExtra(DeleteNotificationReceiver.EXTRA_IDS, ids)
.putExtra(DeleteNotificationReceiver.EXTRA_MMS, mms)
.makeUniqueToPreventMerging()
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
}
fun getMarkAsReadIntent(context: Context): PendingIntent? {
val threadArray = LongArray(conversations.size)
conversations.forEachIndexed { index, conversation ->
threadArray[index] = conversation.threadId
}
val intent = Intent(context, MarkReadReceiver::class.java).setAction(MarkReadReceiver.CLEAR_ACTION)
.putExtra(MarkReadReceiver.THREAD_IDS_EXTRA, threadArray)
.putExtra(MarkReadReceiver.NOTIFICATION_ID_EXTRA, NotificationIds.MESSAGE_SUMMARY)
.makeUniqueToPreventMerging()
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
}
companion object {
val EMPTY = NotificationStateV2(emptyList())
}
}

Wyświetl plik

@ -18,6 +18,10 @@ public class NotificationPrivacyPreference {
return "all".equals(preference);
}
public boolean isDisplayNothing() {
return !isDisplayContact();
}
@Override
public @NonNull String toString() {
return preference;

Wyświetl plik

@ -244,9 +244,9 @@ public final class ConversationUtil {
/**
* @return A Person object representing the given Recipient
*/
@RequiresApi(CONVERSATION_SUPPORT_VERSION)
@RequiresApi(28)
@WorkerThread
private static @NonNull Person buildPerson(@NonNull Context context, @NonNull Recipient recipient) {
public static @NonNull Person buildPerson(@NonNull Context context, @NonNull Recipient recipient) {
return new Person.Builder()
.setKey(getShortcutId(recipient.getId()))
.setName(recipient.getDisplayName(context))

Wyświetl plik

@ -1,5 +1,6 @@
package org.thoughtcrime.securesms.util;
import android.os.Build;
import android.text.TextUtils;
import androidx.annotation.NonNull;
@ -75,6 +76,7 @@ public final class FeatureFlags {
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 STORAGE_SYNC_V2 = "android.storageSyncV2.2";
private static final String NOTIFICATION_REWRITE = "android.notificationRewrite";
/**
* We will only store remote values for flags in this set. If you want a flag to be controllable
@ -106,7 +108,8 @@ public final class FeatureFlags {
ANIMATED_STICKER_MIN_TOTAL_MEMORY,
MESSAGE_PROCESSOR_ALARM_INTERVAL,
MESSAGE_PROCESSOR_DELAY,
STORAGE_SYNC_V2
STORAGE_SYNC_V2,
NOTIFICATION_REWRITE
);
@VisibleForTesting
@ -150,7 +153,8 @@ public final class FeatureFlags {
MESSAGE_PROCESSOR_ALARM_INTERVAL,
MESSAGE_PROCESSOR_DELAY,
GV1_FORCED_MIGRATE,
STORAGE_SYNC_V2
STORAGE_SYNC_V2,
NOTIFICATION_REWRITE
);
/**
@ -342,6 +346,11 @@ public final class FeatureFlags {
return getBoolean(STORAGE_SYNC_V2, false);
}
/** Whether or not to use the new notification system. */
public static boolean useNewNotificationSystem() {
return getBoolean(NOTIFICATION_REWRITE, false) && Build.VERSION.SDK_INT >= 26;
}
/** Only for rendering debug info. */
public static synchronized @NonNull Map<String, Object> getMemoryValues() {
return new TreeMap<>(REMOTE_VALUES);

Wyświetl plik

@ -1,4 +1,5 @@
buildscript {
ext.kotlin_version = '1.4.32'
repositories {
google()
mavenCentral()
@ -8,11 +9,19 @@ buildscript {
includeGroupByRegex "com\\.archinamon.*"
}
}
maven {
url "https://plugins.gradle.org/m2/"
content {
includeGroupByRegex "org\\.jlleitschuh\\.gradle.*"
}
}
}
dependencies {
classpath 'com.android.tools.build:gradle:4.1.1'
classpath 'androidx.navigation:navigation-safe-args-gradle-plugin:2.1.0'
classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.10'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jlleitschuh.gradle:ktlint-gradle:10.0.0"
}
}
@ -64,6 +73,7 @@ task qa {
description 'Quality Assurance. Run before pushing.'
dependsOn ':Signal-Android:testPlayProdReleaseUnitTest',
':Signal-Android:lintPlayProdRelease',
'Signal-Android:ktlintCheck',
':libsignal-service:test',
':Signal-Android:assemblePlayProdDebug'
}

Wyświetl plik

@ -15,6 +15,7 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.uast.UCallExpression;
import org.jetbrains.uast.UExpression;
import org.jetbrains.uast.java.JavaUSimpleNameReferenceExpression;
import org.jetbrains.uast.kotlin.KotlinUSimpleReferenceExpression;
import java.util.Arrays;
import java.util.List;
@ -73,7 +74,7 @@ public final class SignalLogDetector extends Detector implements Detector.UastSc
if (evaluator.isMemberInClass(method, "org.signal.core.util.logging.Log")) {
List<UExpression> arguments = call.getValueArguments();
UExpression tag = arguments.get(0);
if (!(tag instanceof JavaUSimpleNameReferenceExpression)) {
if (!(tag instanceof JavaUSimpleNameReferenceExpression || tag instanceof KotlinUSimpleReferenceExpression)) {
context.report(INLINE_TAG, call, context.getLocation(call), "Not using a tag constant");
}
}

Wyświetl plik

@ -8,6 +8,7 @@ import java.io.InputStream;
import java.util.Scanner;
import static com.android.tools.lint.checks.infrastructure.TestFiles.java;
import static com.android.tools.lint.checks.infrastructure.TestFiles.kotlin;
import static com.android.tools.lint.checks.infrastructure.TestLintTask.lint;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
@ -133,6 +134,24 @@ public final class LogDetectorTest {
.expectClean();
}
@Test
public void log_uses_tag_constant_kotlin() {
lint()
.files(appLogStub,
kotlin("package foo\n" +
"import org.signal.core.util.logging.Log\n" +
"class Example {\n" +
" const val TAG: String = Log.tag(Example::class.java)\n" +
" fun log() {\n" +
" Log.d(TAG, \"msg\")\n" +
" }\n" +
"}")
)
.issues(SignalLogDetector.INLINE_TAG)
.run()
.expectClean();
}
@Test
public void log_uses_inline_tag() {
lint()
@ -154,6 +173,26 @@ public final class LogDetectorTest {
.expectFixDiffs("");
}
@Test
public void log_uses_inline_tag_kotlin() {
lint()
.files(appLogStub,
kotlin("package foo\n" +
"import org.signal.core.util.logging.Log\n" +
"class Example {\n" +
" fun log() {\n" +
" Log.d(\"TAG\", \"msg\")\n" +
" }\n" +
"}"))
.issues(SignalLogDetector.INLINE_TAG)
.run()
.expect("src/foo/Example.kt:5: Error: Not using a tag constant [LogTagInlined]\n" +
" Log.d(\"TAG\", \"msg\")\n" +
" ~~~~~~~~~~~~~~~~~~~\n" +
"1 errors, 0 warnings")
.expectFixDiffs("");
}
@Test
public void glideLogUsed_LogNotSignal_2_args() {
lint()