Create NotificationFactory
This commit is contained in:
@@ -19,6 +19,7 @@ package io.element.android.libraries.androidutils.system
|
||||
import android.annotation.SuppressLint
|
||||
import android.annotation.TargetApi
|
||||
import android.app.Activity
|
||||
import android.app.NotificationManager
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
@@ -72,6 +73,17 @@ fun Context.getApplicationLabel(packageName: String): String {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true it the user has enabled the do not disturb mode.
|
||||
*/
|
||||
fun isDoNotDisturbModeOn(context: Context): Boolean {
|
||||
// We cannot use NotificationManagerCompat here.
|
||||
val setting = context.getSystemService<NotificationManager>()!!.currentInterruptionFilter
|
||||
|
||||
return setting == NotificationManager.INTERRUPTION_FILTER_NONE ||
|
||||
setting == NotificationManager.INTERRUPTION_FILTER_ALARMS
|
||||
}
|
||||
|
||||
/**
|
||||
* display the system dialog for granting this permission. If previously granted, the
|
||||
* system will not show it (so you should call this method).
|
||||
|
||||
@@ -19,6 +19,7 @@ package io.element.android.libraries.push.impl.notifications
|
||||
import android.app.Notification
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.push.impl.notifications.factories.NotificationFactory
|
||||
import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
|
||||
import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent
|
||||
@@ -26,8 +27,9 @@ import javax.inject.Inject
|
||||
|
||||
private typealias ProcessedMessageEvents = List<ProcessedEvent<NotifiableMessageEvent>>
|
||||
|
||||
// TODO Find a better name, it clashes with io.element.android.libraries.push.impl.notifications.factories.NotificationFactory
|
||||
class NotificationFactory @Inject constructor(
|
||||
private val notificationUtils: NotificationUtils,
|
||||
private val notificationFactory: NotificationFactory,
|
||||
private val roomGroupMessageCreator: RoomGroupMessageCreator,
|
||||
private val summaryGroupMessageCreator: SummaryGroupMessageCreator
|
||||
) {
|
||||
@@ -66,7 +68,7 @@ class NotificationFactory @Inject constructor(
|
||||
when (processed) {
|
||||
ProcessedEvent.Type.REMOVE -> OneShotNotification.Removed(key = event.roomId.value)
|
||||
ProcessedEvent.Type.KEEP -> OneShotNotification.Append(
|
||||
notificationUtils.buildRoomInvitationNotification(event),
|
||||
notificationFactory.createRoomInvitationNotification(event),
|
||||
OneShotNotification.Append.Meta(
|
||||
key = event.roomId.value,
|
||||
summaryLine = event.description,
|
||||
@@ -84,7 +86,7 @@ class NotificationFactory @Inject constructor(
|
||||
when (processed) {
|
||||
ProcessedEvent.Type.REMOVE -> OneShotNotification.Removed(key = event.eventId.value)
|
||||
ProcessedEvent.Type.KEEP -> OneShotNotification.Append(
|
||||
notificationUtils.buildSimpleEventNotification(event),
|
||||
notificationFactory.createSimpleEventNotification(event),
|
||||
OneShotNotification.Append.Meta(
|
||||
key = event.eventId.value,
|
||||
summaryLine = event.description,
|
||||
|
||||
@@ -20,47 +20,18 @@ package io.element.android.libraries.push.impl.notifications
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Notification
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.di.ApplicationContext
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.core.ThreadId
|
||||
import io.element.android.libraries.push.impl.R
|
||||
import io.element.android.libraries.push.impl.notifications.channels.NotificationChannels
|
||||
import io.element.android.libraries.push.impl.notifications.factories.PendingIntentFactory
|
||||
import io.element.android.libraries.push.impl.notifications.factories.action.AcceptInvitationActionFactory
|
||||
import io.element.android.libraries.push.impl.notifications.factories.action.MarkAsReadActionFactory
|
||||
import io.element.android.libraries.push.impl.notifications.factories.action.QuickReplyActionFactory
|
||||
import io.element.android.libraries.push.impl.notifications.factories.action.RejectInvitationActionFactory
|
||||
import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent
|
||||
import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent
|
||||
import io.element.android.services.toolbox.api.strings.StringProvider
|
||||
import io.element.android.libraries.push.impl.notifications.factories.NotificationFactory
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
class NotificationUtils @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val notificationChannels: NotificationChannels,
|
||||
// private val vectorPreferences: VectorPreferences,
|
||||
private val stringProvider: StringProvider,
|
||||
private val buildMeta: BuildMeta,
|
||||
private val pendingIntentFactory: PendingIntentFactory,
|
||||
private val markAsReadActionFactory: MarkAsReadActionFactory,
|
||||
private val quickReplyActionFactory: QuickReplyActionFactory,
|
||||
private val rejectInvitationActionFactory: RejectInvitationActionFactory,
|
||||
private val acceptInvitationActionFactory: AcceptInvitationActionFactory,
|
||||
private val notificationFactory: NotificationFactory,
|
||||
) {
|
||||
|
||||
companion object {
|
||||
@@ -79,228 +50,6 @@ class NotificationUtils @Inject constructor(
|
||||
|
||||
private val notificationManager = NotificationManagerCompat.from(context)
|
||||
|
||||
/**
|
||||
* Build a notification for a Room.
|
||||
*/
|
||||
fun buildMessagesListNotification(
|
||||
messageStyle: NotificationCompat.MessagingStyle,
|
||||
roomInfo: RoomEventGroupInfo,
|
||||
threadId: ThreadId?,
|
||||
largeIcon: Bitmap?,
|
||||
lastMessageTimestamp: Long,
|
||||
tickerText: String
|
||||
): Notification {
|
||||
val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color)
|
||||
// Build the pending intent for when the notification is clicked
|
||||
val openIntent = when {
|
||||
threadId != null &&
|
||||
true
|
||||
/** TODO EAx vectorPreferences.areThreadMessagesEnabled() */
|
||||
-> pendingIntentFactory.createOpenThreadPendingIntent(roomInfo, threadId)
|
||||
|
||||
else -> pendingIntentFactory.createOpenRoomPendingIntent(roomInfo.sessionId, roomInfo.roomId)
|
||||
}
|
||||
|
||||
val smallIcon = R.drawable.ic_notification
|
||||
|
||||
val channelId = notificationChannels.getChannelIdForMessage(roomInfo.shouldBing)
|
||||
return NotificationCompat.Builder(context, channelId)
|
||||
.setOnlyAlertOnce(roomInfo.isUpdated)
|
||||
.setWhen(lastMessageTimestamp)
|
||||
// MESSAGING_STYLE sets title and content for API 16 and above devices.
|
||||
.setStyle(messageStyle)
|
||||
// A category allows groups of notifications to be ranked and filtered – per user or system settings.
|
||||
// For example, alarm notifications should display before promo notifications, or message from known contact
|
||||
// that can be displayed in not disturb mode if white listed (the later will need compat28.x)
|
||||
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
|
||||
// ID of the corresponding shortcut, for conversation features under API 30+
|
||||
.setShortcutId(roomInfo.roomId.value)
|
||||
// Title for API < 16 devices.
|
||||
.setContentTitle(roomInfo.roomDisplayName)
|
||||
// Content for API < 16 devices.
|
||||
.setContentText(stringProvider.getString(R.string.notification_new_messages))
|
||||
// Number of new notifications for API <24 (M and below) devices.
|
||||
.setSubText(
|
||||
stringProvider.getQuantityString(
|
||||
R.plurals.notification_new_messages_for_room,
|
||||
messageStyle.messages.size,
|
||||
messageStyle.messages.size
|
||||
)
|
||||
)
|
||||
// Auto-bundling is enabled for 4 or more notifications on API 24+ (N+)
|
||||
// devices and all Wear devices. But we want a custom grouping, so we specify the groupID
|
||||
.setGroup(roomInfo.sessionId.value)
|
||||
// In order to avoid notification making sound twice (due to the summary notification)
|
||||
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL)
|
||||
.setSmallIcon(smallIcon)
|
||||
// Set primary color (important for Wear 2.0 Notifications).
|
||||
.setColor(accentColor)
|
||||
// Sets priority for 25 and below. For 26 and above, 'priority' is deprecated for
|
||||
// 'importance' which is set in the NotificationChannel. The integers representing
|
||||
// 'priority' are different from 'importance', so make sure you don't mix them.
|
||||
.apply {
|
||||
if (roomInfo.shouldBing) {
|
||||
// Compat
|
||||
priority = NotificationCompat.PRIORITY_DEFAULT
|
||||
/*
|
||||
vectorPreferences.getNotificationRingTone()?.let {
|
||||
setSound(it)
|
||||
}
|
||||
*/
|
||||
setLights(accentColor, 500, 500)
|
||||
} else {
|
||||
priority = NotificationCompat.PRIORITY_LOW
|
||||
}
|
||||
|
||||
// Add actions and notification intents
|
||||
// Mark room as read
|
||||
addAction(markAsReadActionFactory.create(roomInfo))
|
||||
// Quick reply
|
||||
if (!roomInfo.hasSmartReplyError) {
|
||||
addAction(quickReplyActionFactory.create(roomInfo, threadId))
|
||||
}
|
||||
if (openIntent != null) {
|
||||
setContentIntent(openIntent)
|
||||
}
|
||||
if (largeIcon != null) {
|
||||
setLargeIcon(largeIcon)
|
||||
}
|
||||
setDeleteIntent(pendingIntentFactory.createDismissRoomPendingIntent(roomInfo.sessionId, roomInfo.roomId))
|
||||
}
|
||||
.setTicker(tickerText)
|
||||
.build()
|
||||
}
|
||||
|
||||
fun buildRoomInvitationNotification(
|
||||
inviteNotifiableEvent: InviteNotifiableEvent
|
||||
): Notification {
|
||||
val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color)
|
||||
val smallIcon = R.drawable.ic_notification
|
||||
val channelId = notificationChannels.getChannelIdForMessage(inviteNotifiableEvent.noisy)
|
||||
return NotificationCompat.Builder(context, channelId)
|
||||
.setOnlyAlertOnce(true)
|
||||
.setContentTitle(inviteNotifiableEvent.roomName ?: buildMeta.applicationName)
|
||||
.setContentText(inviteNotifiableEvent.description)
|
||||
.setGroup(inviteNotifiableEvent.sessionId.value)
|
||||
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL)
|
||||
.setSmallIcon(smallIcon)
|
||||
.setColor(accentColor)
|
||||
.addAction(rejectInvitationActionFactory.create(inviteNotifiableEvent))
|
||||
.addAction(acceptInvitationActionFactory.create(inviteNotifiableEvent))
|
||||
.apply {
|
||||
/*
|
||||
// Build the pending intent for when the notification is clicked
|
||||
val contentIntent = HomeActivity.newIntent(
|
||||
context,
|
||||
firstStartMainActivity = true,
|
||||
inviteNotificationRoomId = inviteNotifiableEvent.roomId
|
||||
)
|
||||
contentIntent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||
// pending intent get reused by system, this will mess up the extra params, so put unique info to avoid that
|
||||
contentIntent.data = createIgnoredUri(inviteNotifiableEvent.eventId)
|
||||
setContentIntent(PendingIntent.getActivity(context, 0, contentIntent, PendingIntentCompat.FLAG_IMMUTABLE))
|
||||
|
||||
*/
|
||||
|
||||
if (inviteNotifiableEvent.noisy) {
|
||||
// Compat
|
||||
priority = NotificationCompat.PRIORITY_DEFAULT
|
||||
/*
|
||||
vectorPreferences.getNotificationRingTone()?.let {
|
||||
setSound(it)
|
||||
}
|
||||
*/
|
||||
setLights(accentColor, 500, 500)
|
||||
} else {
|
||||
priority = NotificationCompat.PRIORITY_LOW
|
||||
}
|
||||
setAutoCancel(true)
|
||||
}
|
||||
.build()
|
||||
}
|
||||
|
||||
fun buildSimpleEventNotification(
|
||||
simpleNotifiableEvent: SimpleNotifiableEvent,
|
||||
): Notification {
|
||||
val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color)
|
||||
val smallIcon = R.drawable.ic_notification
|
||||
|
||||
val channelId = notificationChannels.getChannelIdForMessage(simpleNotifiableEvent.noisy)
|
||||
return NotificationCompat.Builder(context, channelId)
|
||||
.setOnlyAlertOnce(true)
|
||||
.setContentTitle(buildMeta.applicationName)
|
||||
.setContentText(simpleNotifiableEvent.description)
|
||||
.setGroup(simpleNotifiableEvent.sessionId.value)
|
||||
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL)
|
||||
.setSmallIcon(smallIcon)
|
||||
.setColor(accentColor)
|
||||
.setAutoCancel(true)
|
||||
.setContentIntent(pendingIntentFactory.createOpenRoomPendingIntent(simpleNotifiableEvent.sessionId, simpleNotifiableEvent.roomId))
|
||||
.apply {
|
||||
if (simpleNotifiableEvent.noisy) {
|
||||
// Compat
|
||||
priority = NotificationCompat.PRIORITY_DEFAULT
|
||||
/*
|
||||
vectorPreferences.getNotificationRingTone()?.let {
|
||||
setSound(it)
|
||||
}
|
||||
*/
|
||||
setLights(accentColor, 500, 500)
|
||||
} else {
|
||||
priority = NotificationCompat.PRIORITY_LOW
|
||||
}
|
||||
setAutoCancel(true)
|
||||
}
|
||||
.build()
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the summary notification.
|
||||
*/
|
||||
fun buildSummaryListNotification(
|
||||
sessionId: SessionId,
|
||||
style: NotificationCompat.InboxStyle?,
|
||||
compatSummary: String,
|
||||
noisy: Boolean,
|
||||
lastMessageTimestamp: Long
|
||||
): Notification {
|
||||
val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color)
|
||||
val smallIcon = R.drawable.ic_notification
|
||||
val channelId = notificationChannels.getChannelIdForMessage(noisy)
|
||||
return NotificationCompat.Builder(context, channelId)
|
||||
.setOnlyAlertOnce(true)
|
||||
// used in compat < N, after summary is built based on child notifications
|
||||
.setWhen(lastMessageTimestamp)
|
||||
.setStyle(style)
|
||||
.setContentTitle(sessionId.value)
|
||||
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
|
||||
.setSmallIcon(smallIcon)
|
||||
// set content text to support devices running API level < 24
|
||||
.setContentText(compatSummary)
|
||||
.setGroup(sessionId.value)
|
||||
// set this notification as the summary for the group
|
||||
.setGroupSummary(true)
|
||||
.setColor(accentColor)
|
||||
.apply {
|
||||
if (noisy) {
|
||||
// Compat
|
||||
priority = NotificationCompat.PRIORITY_DEFAULT
|
||||
/*
|
||||
vectorPreferences.getNotificationRingTone()?.let {
|
||||
setSound(it)
|
||||
}
|
||||
*/
|
||||
setLights(accentColor, 500, 500)
|
||||
} else {
|
||||
// compat
|
||||
priority = NotificationCompat.PRIORITY_LOW
|
||||
}
|
||||
}
|
||||
.setContentIntent(pendingIntentFactory.createOpenSessionPendingIntent(sessionId))
|
||||
.setDeleteIntent(pendingIntentFactory.createDismissSummaryPendingIntent(sessionId))
|
||||
.build()
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel the foreground notification service.
|
||||
*/
|
||||
@@ -329,55 +78,7 @@ class NotificationUtils @Inject constructor(
|
||||
notificationManager.notify(
|
||||
"DIAGNOSTIC",
|
||||
888,
|
||||
NotificationCompat.Builder(context, notificationChannels.getChannelIdForTest())
|
||||
.setContentTitle(buildMeta.applicationName)
|
||||
.setContentText(stringProvider.getString(R.string.notification_test_push_notification_content))
|
||||
.setSmallIcon(R.drawable.ic_notification)
|
||||
.setLargeIcon(getBitmap(context, R.drawable.element_logo_green))
|
||||
.setColor(ContextCompat.getColor(context, R.color.notification_accent_color))
|
||||
.setPriority(NotificationCompat.PRIORITY_MAX)
|
||||
.setCategory(NotificationCompat.CATEGORY_STATUS)
|
||||
.setAutoCancel(true)
|
||||
.setContentIntent(pendingIntentFactory.createTestPendingIntent())
|
||||
.build()
|
||||
notificationFactory.createDiagnosticNotification()
|
||||
)
|
||||
}
|
||||
|
||||
private fun getBitmap(context: Context, @DrawableRes drawableRes: Int): Bitmap? {
|
||||
val drawable = ResourcesCompat.getDrawable(context.resources, drawableRes, null) ?: return null
|
||||
val canvas = Canvas()
|
||||
val bitmap = Bitmap.createBitmap(drawable.intrinsicWidth, drawable.intrinsicHeight, Bitmap.Config.ARGB_8888)
|
||||
canvas.setBitmap(bitmap)
|
||||
drawable.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)
|
||||
drawable.draw(canvas)
|
||||
return bitmap
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true it the user has enabled the do not disturb mode.
|
||||
*/
|
||||
fun isDoNotDisturbModeOn(): Boolean {
|
||||
// We cannot use NotificationManagerCompat here.
|
||||
val setting = context.getSystemService<NotificationManager>()!!.currentInterruptionFilter
|
||||
|
||||
return setting == NotificationManager.INTERRUPTION_FILTER_NONE ||
|
||||
setting == NotificationManager.INTERRUPTION_FILTER_ALARMS
|
||||
}
|
||||
|
||||
/*
|
||||
private fun getActionText(@StringRes stringRes: Int, @AttrRes colorRes: Int): Spannable {
|
||||
return SpannableString(context.getText(stringRes)).apply {
|
||||
val foregroundColorSpan = ForegroundColorSpan(ThemeUtils.getColor(context, colorRes))
|
||||
setSpan(foregroundColorSpan, 0, length, 0)
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
private fun ensureTitleNotEmpty(title: String?): CharSequence {
|
||||
if (title.isNullOrBlank()) {
|
||||
return buildMeta.applicationName
|
||||
}
|
||||
|
||||
return title
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import androidx.core.app.Person
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.push.impl.R
|
||||
import io.element.android.libraries.push.impl.notifications.factories.NotificationFactory
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
|
||||
import io.element.android.services.toolbox.api.strings.StringProvider
|
||||
import me.gujun.android.span.Span
|
||||
@@ -32,7 +33,7 @@ import javax.inject.Inject
|
||||
class RoomGroupMessageCreator @Inject constructor(
|
||||
private val bitmapLoader: NotificationBitmapLoader,
|
||||
private val stringProvider: StringProvider,
|
||||
private val notificationUtils: NotificationUtils
|
||||
private val notificationFactory: NotificationFactory
|
||||
) {
|
||||
|
||||
fun createRoomMessage(
|
||||
@@ -76,7 +77,7 @@ class RoomGroupMessageCreator @Inject constructor(
|
||||
shouldBing = events.any { it.noisy }
|
||||
)
|
||||
return RoomNotification.Message(
|
||||
notificationUtils.buildMessagesListNotification(
|
||||
notificationFactory.createMessagesListNotification(
|
||||
style,
|
||||
RoomEventGroupInfo(
|
||||
sessionId = sessionId,
|
||||
|
||||
@@ -20,6 +20,7 @@ import android.app.Notification
|
||||
import androidx.core.app.NotificationCompat
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.push.impl.R
|
||||
import io.element.android.libraries.push.impl.notifications.factories.NotificationFactory
|
||||
import io.element.android.services.toolbox.api.strings.StringProvider
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -39,7 +40,7 @@ import javax.inject.Inject
|
||||
*/
|
||||
class SummaryGroupMessageCreator @Inject constructor(
|
||||
private val stringProvider: StringProvider,
|
||||
private val notificationUtils: NotificationUtils
|
||||
private val notificationFactory: NotificationFactory
|
||||
) {
|
||||
|
||||
fun createSummaryNotification(
|
||||
@@ -72,7 +73,7 @@ class SummaryGroupMessageCreator @Inject constructor(
|
||||
// TODO get latest event?
|
||||
.setSummaryText(stringProvider.getQuantityString(R.plurals.notification_unread_notified_messages, nbEvents, nbEvents))
|
||||
return if (useCompleteNotificationFormat) {
|
||||
notificationUtils.buildSummaryListNotification(
|
||||
notificationFactory.createSummaryListNotification(
|
||||
sessionId,
|
||||
summaryInboxStyle,
|
||||
sumTitle,
|
||||
@@ -165,7 +166,7 @@ class SummaryGroupMessageCreator @Inject constructor(
|
||||
messageStr
|
||||
}
|
||||
}
|
||||
return notificationUtils.buildSummaryListNotification(
|
||||
return notificationFactory.createSummaryListNotification(
|
||||
sessionId = sessionId,
|
||||
style = null,
|
||||
compatSummary = privacyTitle,
|
||||
|
||||
@@ -0,0 +1,295 @@
|
||||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.push.impl.notifications.factories
|
||||
|
||||
import android.app.Notification
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.di.ApplicationContext
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.core.ThreadId
|
||||
import io.element.android.libraries.push.impl.R
|
||||
import io.element.android.libraries.push.impl.notifications.RoomEventGroupInfo
|
||||
import io.element.android.libraries.push.impl.notifications.channels.NotificationChannels
|
||||
import io.element.android.libraries.push.impl.notifications.factories.action.AcceptInvitationActionFactory
|
||||
import io.element.android.libraries.push.impl.notifications.factories.action.MarkAsReadActionFactory
|
||||
import io.element.android.libraries.push.impl.notifications.factories.action.QuickReplyActionFactory
|
||||
import io.element.android.libraries.push.impl.notifications.factories.action.RejectInvitationActionFactory
|
||||
import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent
|
||||
import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent
|
||||
import io.element.android.services.toolbox.api.strings.StringProvider
|
||||
import javax.inject.Inject
|
||||
|
||||
class NotificationFactory @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val notificationChannels: NotificationChannels,
|
||||
private val stringProvider: StringProvider,
|
||||
private val buildMeta: BuildMeta,
|
||||
private val pendingIntentFactory: PendingIntentFactory,
|
||||
private val markAsReadActionFactory: MarkAsReadActionFactory,
|
||||
private val quickReplyActionFactory: QuickReplyActionFactory,
|
||||
private val rejectInvitationActionFactory: RejectInvitationActionFactory,
|
||||
private val acceptInvitationActionFactory: AcceptInvitationActionFactory,
|
||||
) {
|
||||
/**
|
||||
* Create a notification for a Room.
|
||||
*/
|
||||
fun createMessagesListNotification(
|
||||
messageStyle: NotificationCompat.MessagingStyle,
|
||||
roomInfo: RoomEventGroupInfo,
|
||||
threadId: ThreadId?,
|
||||
largeIcon: Bitmap?,
|
||||
lastMessageTimestamp: Long,
|
||||
tickerText: String
|
||||
): Notification {
|
||||
val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color)
|
||||
// Build the pending intent for when the notification is clicked
|
||||
val openIntent = when {
|
||||
threadId != null -> pendingIntentFactory.createOpenThreadPendingIntent(roomInfo, threadId)
|
||||
else -> pendingIntentFactory.createOpenRoomPendingIntent(roomInfo.sessionId, roomInfo.roomId)
|
||||
}
|
||||
|
||||
val smallIcon = R.drawable.ic_notification
|
||||
|
||||
val channelId = notificationChannels.getChannelIdForMessage(roomInfo.shouldBing)
|
||||
return NotificationCompat.Builder(context, channelId)
|
||||
.setOnlyAlertOnce(roomInfo.isUpdated)
|
||||
.setWhen(lastMessageTimestamp)
|
||||
// MESSAGING_STYLE sets title and content for API 16 and above devices.
|
||||
.setStyle(messageStyle)
|
||||
// A category allows groups of notifications to be ranked and filtered – per user or system settings.
|
||||
// For example, alarm notifications should display before promo notifications, or message from known contact
|
||||
// that can be displayed in not disturb mode if white listed (the later will need compat28.x)
|
||||
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
|
||||
// ID of the corresponding shortcut, for conversation features under API 30+
|
||||
.setShortcutId(roomInfo.roomId.value)
|
||||
// Title for API < 16 devices.
|
||||
.setContentTitle(roomInfo.roomDisplayName)
|
||||
// Content for API < 16 devices.
|
||||
.setContentText(stringProvider.getString(R.string.notification_new_messages))
|
||||
// Number of new notifications for API <24 (M and below) devices.
|
||||
.setSubText(
|
||||
stringProvider.getQuantityString(
|
||||
R.plurals.notification_new_messages_for_room,
|
||||
messageStyle.messages.size,
|
||||
messageStyle.messages.size
|
||||
)
|
||||
)
|
||||
// Auto-bundling is enabled for 4 or more notifications on API 24+ (N+)
|
||||
// devices and all Wear devices. But we want a custom grouping, so we specify the groupID
|
||||
.setGroup(roomInfo.sessionId.value)
|
||||
// In order to avoid notification making sound twice (due to the summary notification)
|
||||
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL)
|
||||
.setSmallIcon(smallIcon)
|
||||
// Set primary color (important for Wear 2.0 Notifications).
|
||||
.setColor(accentColor)
|
||||
// Sets priority for 25 and below. For 26 and above, 'priority' is deprecated for
|
||||
// 'importance' which is set in the NotificationChannel. The integers representing
|
||||
// 'priority' are different from 'importance', so make sure you don't mix them.
|
||||
.apply {
|
||||
if (roomInfo.shouldBing) {
|
||||
// Compat
|
||||
priority = NotificationCompat.PRIORITY_DEFAULT
|
||||
/*
|
||||
vectorPreferences.getNotificationRingTone()?.let {
|
||||
setSound(it)
|
||||
}
|
||||
*/
|
||||
setLights(accentColor, 500, 500)
|
||||
} else {
|
||||
priority = NotificationCompat.PRIORITY_LOW
|
||||
}
|
||||
|
||||
// Add actions and notification intents
|
||||
// Mark room as read
|
||||
addAction(markAsReadActionFactory.create(roomInfo))
|
||||
// Quick reply
|
||||
if (!roomInfo.hasSmartReplyError) {
|
||||
addAction(quickReplyActionFactory.create(roomInfo, threadId))
|
||||
}
|
||||
if (openIntent != null) {
|
||||
setContentIntent(openIntent)
|
||||
}
|
||||
if (largeIcon != null) {
|
||||
setLargeIcon(largeIcon)
|
||||
}
|
||||
setDeleteIntent(pendingIntentFactory.createDismissRoomPendingIntent(roomInfo.sessionId, roomInfo.roomId))
|
||||
}
|
||||
.setTicker(tickerText)
|
||||
.build()
|
||||
}
|
||||
|
||||
fun createRoomInvitationNotification(
|
||||
inviteNotifiableEvent: InviteNotifiableEvent
|
||||
): Notification {
|
||||
val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color)
|
||||
val smallIcon = R.drawable.ic_notification
|
||||
val channelId = notificationChannels.getChannelIdForMessage(inviteNotifiableEvent.noisy)
|
||||
return NotificationCompat.Builder(context, channelId)
|
||||
.setOnlyAlertOnce(true)
|
||||
.setContentTitle(inviteNotifiableEvent.roomName ?: buildMeta.applicationName)
|
||||
.setContentText(inviteNotifiableEvent.description)
|
||||
.setGroup(inviteNotifiableEvent.sessionId.value)
|
||||
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL)
|
||||
.setSmallIcon(smallIcon)
|
||||
.setColor(accentColor)
|
||||
.addAction(rejectInvitationActionFactory.create(inviteNotifiableEvent))
|
||||
.addAction(acceptInvitationActionFactory.create(inviteNotifiableEvent))
|
||||
.apply {
|
||||
/*
|
||||
// Build the pending intent for when the notification is clicked
|
||||
val contentIntent = HomeActivity.newIntent(
|
||||
context,
|
||||
firstStartMainActivity = true,
|
||||
inviteNotificationRoomId = inviteNotifiableEvent.roomId
|
||||
)
|
||||
contentIntent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||
// pending intent get reused by system, this will mess up the extra params, so put unique info to avoid that
|
||||
contentIntent.data = createIgnoredUri(inviteNotifiableEvent.eventId)
|
||||
setContentIntent(PendingIntent.getActivity(context, 0, contentIntent, PendingIntentCompat.FLAG_IMMUTABLE))
|
||||
|
||||
*/
|
||||
|
||||
if (inviteNotifiableEvent.noisy) {
|
||||
// Compat
|
||||
priority = NotificationCompat.PRIORITY_DEFAULT
|
||||
/*
|
||||
vectorPreferences.getNotificationRingTone()?.let {
|
||||
setSound(it)
|
||||
}
|
||||
*/
|
||||
setLights(accentColor, 500, 500)
|
||||
} else {
|
||||
priority = NotificationCompat.PRIORITY_LOW
|
||||
}
|
||||
setAutoCancel(true)
|
||||
}
|
||||
.build()
|
||||
}
|
||||
|
||||
fun createSimpleEventNotification(
|
||||
simpleNotifiableEvent: SimpleNotifiableEvent,
|
||||
): Notification {
|
||||
val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color)
|
||||
val smallIcon = R.drawable.ic_notification
|
||||
|
||||
val channelId = notificationChannels.getChannelIdForMessage(simpleNotifiableEvent.noisy)
|
||||
return NotificationCompat.Builder(context, channelId)
|
||||
.setOnlyAlertOnce(true)
|
||||
.setContentTitle(buildMeta.applicationName)
|
||||
.setContentText(simpleNotifiableEvent.description)
|
||||
.setGroup(simpleNotifiableEvent.sessionId.value)
|
||||
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL)
|
||||
.setSmallIcon(smallIcon)
|
||||
.setColor(accentColor)
|
||||
.setAutoCancel(true)
|
||||
.setContentIntent(pendingIntentFactory.createOpenRoomPendingIntent(simpleNotifiableEvent.sessionId, simpleNotifiableEvent.roomId))
|
||||
.apply {
|
||||
if (simpleNotifiableEvent.noisy) {
|
||||
// Compat
|
||||
priority = NotificationCompat.PRIORITY_DEFAULT
|
||||
/*
|
||||
vectorPreferences.getNotificationRingTone()?.let {
|
||||
setSound(it)
|
||||
}
|
||||
*/
|
||||
setLights(accentColor, 500, 500)
|
||||
} else {
|
||||
priority = NotificationCompat.PRIORITY_LOW
|
||||
}
|
||||
setAutoCancel(true)
|
||||
}
|
||||
.build()
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the summary notification.
|
||||
*/
|
||||
fun createSummaryListNotification(
|
||||
sessionId: SessionId,
|
||||
style: NotificationCompat.InboxStyle?,
|
||||
compatSummary: String,
|
||||
noisy: Boolean,
|
||||
lastMessageTimestamp: Long
|
||||
): Notification {
|
||||
val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color)
|
||||
val smallIcon = R.drawable.ic_notification
|
||||
val channelId = notificationChannels.getChannelIdForMessage(noisy)
|
||||
return NotificationCompat.Builder(context, channelId)
|
||||
.setOnlyAlertOnce(true)
|
||||
// used in compat < N, after summary is built based on child notifications
|
||||
.setWhen(lastMessageTimestamp)
|
||||
.setStyle(style)
|
||||
.setContentTitle(sessionId.value)
|
||||
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
|
||||
.setSmallIcon(smallIcon)
|
||||
// set content text to support devices running API level < 24
|
||||
.setContentText(compatSummary)
|
||||
.setGroup(sessionId.value)
|
||||
// set this notification as the summary for the group
|
||||
.setGroupSummary(true)
|
||||
.setColor(accentColor)
|
||||
.apply {
|
||||
if (noisy) {
|
||||
// Compat
|
||||
priority = NotificationCompat.PRIORITY_DEFAULT
|
||||
/*
|
||||
vectorPreferences.getNotificationRingTone()?.let {
|
||||
setSound(it)
|
||||
}
|
||||
*/
|
||||
setLights(accentColor, 500, 500)
|
||||
} else {
|
||||
// compat
|
||||
priority = NotificationCompat.PRIORITY_LOW
|
||||
}
|
||||
}
|
||||
.setContentIntent(pendingIntentFactory.createOpenSessionPendingIntent(sessionId))
|
||||
.setDeleteIntent(pendingIntentFactory.createDismissSummaryPendingIntent(sessionId))
|
||||
.build()
|
||||
}
|
||||
|
||||
fun createDiagnosticNotification(): Notification {
|
||||
return NotificationCompat.Builder(context, notificationChannels.getChannelIdForTest())
|
||||
.setContentTitle(buildMeta.applicationName)
|
||||
.setContentText(stringProvider.getString(R.string.notification_test_push_notification_content))
|
||||
.setSmallIcon(R.drawable.ic_notification)
|
||||
.setLargeIcon(getBitmap(context, R.drawable.element_logo_green))
|
||||
.setColor(ContextCompat.getColor(context, R.color.notification_accent_color))
|
||||
.setPriority(NotificationCompat.PRIORITY_MAX)
|
||||
.setCategory(NotificationCompat.CATEGORY_STATUS)
|
||||
.setAutoCancel(true)
|
||||
.setContentIntent(pendingIntentFactory.createTestPendingIntent())
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun getBitmap(context: Context, @DrawableRes drawableRes: Int): Bitmap? {
|
||||
val drawable = ResourcesCompat.getDrawable(context.resources, drawableRes, null) ?: return null
|
||||
val canvas = Canvas()
|
||||
val bitmap = Bitmap.createBitmap(drawable.intrinsicWidth, drawable.intrinsicHeight, Bitmap.Config.ARGB_8888)
|
||||
canvas.setBitmap(bitmap)
|
||||
drawable.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)
|
||||
drawable.draw(canvas)
|
||||
return bitmap
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user