Signal-Android/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationBuilder.kt

650 wiersze
23 KiB
Kotlin

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
}
}