Merge pull request #566 from vector-im/feature/redact_notification

Redact notification
This commit is contained in:
Benoit Marty 2019-09-19 13:02:17 +02:00 committed by GitHub
commit c728834273
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 230 additions and 215 deletions

View File

@ -7,6 +7,7 @@ Features:
Improvements: Improvements:
- Add unread indent on room list (#485) - Add unread indent on room list (#485)
- Message Editing: Update notifications (#128) - Message Editing: Update notifications (#128)
- Remove any notification of a redacted event (#563)
Other changes: Other changes:
- -

View File

@ -43,6 +43,7 @@ interface PushRuleService {
interface PushRuleListener { interface PushRuleListener {
fun onMatchRule(event: Event, actions: List<Action>) fun onMatchRule(event: Event, actions: List<Action>)
fun onRoomLeft(roomId: String) fun onRoomLeft(roomId: String)
fun onEventRedacted(redactedEventId: String)
fun batchFinish() fun batchFinish()
} }
} }

View File

@ -132,6 +132,16 @@ internal class DefaultPushRuleService @Inject constructor(
} }
} }
fun dispatchRedactedEventId(redactedEventId: String) {
try {
listeners.forEach {
it.onEventRedacted(redactedEventId)
}
} catch (e: Throwable) {
Timber.e(e, "Error while dispatching room left")
}
}
fun dispatchFinish() { fun dispatchFinish() {
try { try {
listeners.forEach { listeners.forEach {

View File

@ -78,6 +78,25 @@ internal class DefaultProcessEventForPushTask @Inject constructor(
defaultPushRuleService.dispatchBing(event, it) defaultPushRuleService.dispatchBing(event, it)
} }
} }
val allRedactedEvents = params.syncResponse.join
.map { entries ->
entries.value.timeline?.events?.filter {
it.type == EventType.REDACTION
}
.orEmpty()
.mapNotNull { it.redacts }
}
.fold(emptyList<String>(), { acc, next ->
acc + next
})
Timber.v("[PushRules] Found ${allRedactedEvents.size} redacted events")
allRedactedEvents.forEach { redactedEventId ->
defaultPushRuleService.dispatchRedactedEventId(redactedEventId)
}
defaultPushRuleService.dispatchFinish() defaultPushRuleService.dispatchFinish()
} }

View File

@ -196,7 +196,6 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
// This ID can and should be used to detect duplicate notification requests. // This ID can and should be used to detect duplicate notification requests.
val eventId = data["event_id"] ?: return //Just ignore val eventId = data["event_id"] ?: return //Just ignore
val eventType = data["type"] val eventType = data["type"]
if (eventType == null) { if (eventType == null) {
//Just add a generic unknown event //Just add a generic unknown event
@ -214,10 +213,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
) )
notificationDrawerManager.onNotifiableEventReceived(simpleNotifiableEvent) notificationDrawerManager.onNotifiableEventReceived(simpleNotifiableEvent)
notificationDrawerManager.refreshNotificationDrawer() notificationDrawerManager.refreshNotificationDrawer()
return
} else { } else {
val event = parseEvent(data) ?: return val event = parseEvent(data) ?: return
val notifiableEvent = notifiableEventResolver.resolveEvent(event, session) val notifiableEvent = notifiableEventResolver.resolveEvent(event, session)
@ -228,8 +224,6 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
Timber.e("--> ${event}") Timber.e("--> ${event}")
} }
} else { } else {
if (notifiableEvent is NotifiableMessageEvent) { if (notifiableEvent is NotifiableMessageEvent) {
if (TextUtils.isEmpty(notifiableEvent.senderName)) { if (TextUtils.isEmpty(notifiableEvent.senderName)) {
notifiableEvent.senderName = data["sender_display_name"] notifiableEvent.senderName = data["sender_display_name"]
@ -246,7 +240,6 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
notificationDrawerManager.refreshNotificationDrawer() notificationDrawerManager.refreshNotificationDrawer()
} }
} }
} }
private fun findRoomNameBestEffort(data: Map<String, String>, session: Session?): String? { private fun findRoomNameBestEffort(data: Map<String, String>, session: Session?): String? {

View File

@ -48,7 +48,6 @@ import im.vector.riotx.features.lifecycle.VectorActivityLifecycleCallbacks
import im.vector.riotx.features.notifications.NotificationDrawerManager import im.vector.riotx.features.notifications.NotificationDrawerManager
import im.vector.riotx.features.notifications.NotificationUtils import im.vector.riotx.features.notifications.NotificationUtils
import im.vector.riotx.features.notifications.PushRuleTriggerListener import im.vector.riotx.features.notifications.PushRuleTriggerListener
import im.vector.riotx.features.rageshake.VectorFileLogger
import im.vector.riotx.features.rageshake.VectorUncaughtExceptionHandler import im.vector.riotx.features.rageshake.VectorUncaughtExceptionHandler
import im.vector.riotx.features.settings.VectorPreferences import im.vector.riotx.features.settings.VectorPreferences
import im.vector.riotx.features.version.VersionProvider import im.vector.riotx.features.version.VersionProvider
@ -73,6 +72,7 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.
@Inject lateinit var pushRuleTriggerListener: PushRuleTriggerListener @Inject lateinit var pushRuleTriggerListener: PushRuleTriggerListener
@Inject lateinit var vectorPreferences: VectorPreferences @Inject lateinit var vectorPreferences: VectorPreferences
@Inject lateinit var versionProvider: VersionProvider @Inject lateinit var versionProvider: VersionProvider
@Inject lateinit var notificationUtils: NotificationUtils
lateinit var vectorComponent: VectorComponent lateinit var vectorComponent: VectorComponent
private var fontThreadHandler: Handler? = null private var fontThreadHandler: Handler? = null
@ -112,7 +112,7 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.
emojiCompatWrapper.init(fontRequest) emojiCompatWrapper.init(fontRequest)
NotificationUtils.createNotificationChannels(applicationContext) notificationUtils.createNotificationChannels()
if (authenticator.hasAuthenticatedSessions() && !activeSessionHolder.hasActiveSession()) { if (authenticator.hasAuthenticatedSessions() && !activeSessionHolder.hasActiveSession()) {
val lastAuthenticatedSession = authenticator.getLastAuthenticatedSession()!! val lastAuthenticatedSession = authenticator.getLastAuthenticatedSession()!!
activeSessionHolder.setActiveSession(lastAuthenticatedSession) activeSessionHolder.setActiveSession(lastAuthenticatedSession)

View File

@ -36,10 +36,7 @@ import im.vector.riotx.features.home.HomeRoomListObservableStore
import im.vector.riotx.features.home.group.SelectedGroupStore import im.vector.riotx.features.home.group.SelectedGroupStore
import im.vector.riotx.features.html.EventHtmlRenderer import im.vector.riotx.features.html.EventHtmlRenderer
import im.vector.riotx.features.navigation.Navigator import im.vector.riotx.features.navigation.Navigator
import im.vector.riotx.features.notifications.NotifiableEventResolver import im.vector.riotx.features.notifications.*
import im.vector.riotx.features.notifications.NotificationBroadcastReceiver
import im.vector.riotx.features.notifications.NotificationDrawerManager
import im.vector.riotx.features.notifications.PushRuleTriggerListener
import im.vector.riotx.features.rageshake.BugReporter import im.vector.riotx.features.rageshake.BugReporter
import im.vector.riotx.features.rageshake.VectorFileLogger import im.vector.riotx.features.rageshake.VectorFileLogger
import im.vector.riotx.features.rageshake.VectorUncaughtExceptionHandler import im.vector.riotx.features.rageshake.VectorUncaughtExceptionHandler
@ -58,6 +55,8 @@ interface VectorComponent {
fun currentSession(): Session fun currentSession(): Session
fun notificationUtils(): NotificationUtils
fun notificationDrawerManager(): NotificationDrawerManager fun notificationDrawerManager(): NotificationDrawerManager
fun appContext(): Context fun appContext(): Context
@ -72,7 +71,7 @@ interface VectorComponent {
fun emojiCompatFontProvider(): EmojiCompatFontProvider fun emojiCompatFontProvider(): EmojiCompatFontProvider
fun emojiCompatWrapper() : EmojiCompatWrapper fun emojiCompatWrapper(): EmojiCompatWrapper
fun eventHtmlRenderer(): EventHtmlRenderer fun eventHtmlRenderer(): EventHtmlRenderer

View File

@ -19,6 +19,7 @@ package im.vector.riotx.core.services
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import im.vector.riotx.core.extensions.vectorComponent
import im.vector.riotx.features.notifications.NotificationUtils import im.vector.riotx.features.notifications.NotificationUtils
import timber.log.Timber import timber.log.Timber
@ -32,11 +33,18 @@ class CallService : VectorService() {
*/ */
private var mCallIdInProgress: String? = null private var mCallIdInProgress: String? = null
private lateinit var notificationUtils: NotificationUtils
/** /**
* incoming (foreground notification) * incoming (foreground notification)
*/ */
private var mIncomingCallId: String? = null private var mIncomingCallId: String? = null
override fun onCreate() {
super.onCreate()
notificationUtils = vectorComponent().notificationUtils()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (intent == null) { if (intent == null) {
// Service started again by the system. // Service started again by the system.
@ -120,7 +128,7 @@ class CallService : VectorService() {
private fun displayCallInProgressNotification(intent: Intent) { private fun displayCallInProgressNotification(intent: Intent) {
val callId = intent.getStringExtra(EXTRA_CALL_ID) val callId = intent.getStringExtra(EXTRA_CALL_ID)
val notification = NotificationUtils.buildPendingCallNotification(applicationContext, val notification = notificationUtils.buildPendingCallNotification(
intent.getBooleanExtra(EXTRA_IS_VIDEO, false), intent.getBooleanExtra(EXTRA_IS_VIDEO, false),
intent.getStringExtra(EXTRA_ROOM_NAME), intent.getStringExtra(EXTRA_ROOM_NAME),
intent.getStringExtra(EXTRA_ROOM_ID), intent.getStringExtra(EXTRA_ROOM_ID),
@ -136,7 +144,7 @@ class CallService : VectorService() {
* Hide the permanent call notifications * Hide the permanent call notifications
*/ */
private fun hideCallNotifications() { private fun hideCallNotifications() {
val notification = NotificationUtils.buildCallEndedNotification(applicationContext) val notification = notificationUtils.buildCallEndedNotification()
// It's mandatory to startForeground to avoid crash // It's mandatory to startForeground to avoid crash
startForeground(NOTIFICATION_ID, notification) startForeground(NOTIFICATION_ID, notification)

View File

@ -18,11 +18,7 @@ package im.vector.riotx.core.utils
import android.annotation.TargetApi import android.annotation.TargetApi
import android.app.Activity import android.app.Activity
import android.content.ActivityNotFoundException import android.content.*
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.PowerManager import android.os.PowerManager
@ -32,7 +28,7 @@ import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.features.notifications.supportNotificationChannels import im.vector.riotx.features.notifications.NotificationUtils
import im.vector.riotx.features.settings.VectorLocale import im.vector.riotx.features.settings.VectorLocale
import timber.log.Timber import timber.log.Timber
import java.util.* import java.util.*
@ -138,7 +134,7 @@ fun startNotificationSettingsIntent(activity: AppCompatActivity, requestCode: In
*/ */
@TargetApi(Build.VERSION_CODES.O) @TargetApi(Build.VERSION_CODES.O)
fun startNotificationChannelSettingsIntent(fragment: Fragment, channelID: String) { fun startNotificationChannelSettingsIntent(fragment: Fragment, channelID: String) {
if (!supportNotificationChannels()) return if (!NotificationUtils.supportNotificationChannels()) return
val intent = Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS).apply { val intent = Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS).apply {
putExtra(Settings.EXTRA_APP_PACKAGE, fragment.context?.packageName) putExtra(Settings.EXTRA_APP_PACKAGE, fragment.context?.packageName)
putExtra(Settings.EXTRA_CHANNEL_ID, channelID) putExtra(Settings.EXTRA_CHANNEL_ID, channelID)

View File

@ -32,6 +32,7 @@ data class InviteNotifiableEvent(
override var isPushGatewayEvent: Boolean = false) : NotifiableEvent { override var isPushGatewayEvent: Boolean = false) : NotifiableEvent {
override var hasBeenDisplayed: Boolean = false override var hasBeenDisplayed: Boolean = false
override var isRedacted: Boolean = false
override var lockScreenVisibility = NotificationCompat.VISIBILITY_PUBLIC override var lockScreenVisibility = NotificationCompat.VISIBILITY_PUBLIC
} }

View File

@ -31,6 +31,7 @@ interface NotifiableEvent : Serializable {
// Compat: Only for android <7, for newer version the sound is defined in the channel // Compat: Only for android <7, for newer version the sound is defined in the channel
var soundName: String? var soundName: String?
var hasBeenDisplayed: Boolean var hasBeenDisplayed: Boolean
var isRedacted: Boolean
//Used to know if event should be replaced with the one coming from eventstream //Used to know if event should be replaced with the one coming from eventstream
var isPushGatewayEvent: Boolean var isPushGatewayEvent: Boolean
} }

View File

@ -36,6 +36,7 @@ data class NotifiableMessageEvent(
override var soundName: String? = null override var soundName: String? = null
override var lockScreenVisibility = NotificationCompat.VISIBILITY_PUBLIC override var lockScreenVisibility = NotificationCompat.VISIBILITY_PUBLIC
override var hasBeenDisplayed: Boolean = false override var hasBeenDisplayed: Boolean = false
override var isRedacted: Boolean = false
var roomAvatarPath: String? = null var roomAvatarPath: String? = null
var senderAvatarPath: String? = null var senderAvatarPath: String? = null

View File

@ -15,7 +15,6 @@
*/ */
package im.vector.riotx.features.notifications package im.vector.riotx.features.notifications
import android.app.Notification
import android.content.Context import android.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import android.os.Handler import android.os.Handler
@ -27,6 +26,7 @@ import im.vector.matrix.android.api.session.content.ContentUrlResolver
import im.vector.riotx.BuildConfig import im.vector.riotx.BuildConfig
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.features.settings.VectorPreferences import im.vector.riotx.features.settings.VectorPreferences
import me.gujun.android.span.span import me.gujun.android.span.span
import timber.log.Timber import timber.log.Timber
@ -43,7 +43,9 @@ import javax.inject.Singleton
*/ */
@Singleton @Singleton
class NotificationDrawerManager @Inject constructor(private val context: Context, class NotificationDrawerManager @Inject constructor(private val context: Context,
private val notificationUtils: NotificationUtils,
private val vectorPreferences: VectorPreferences, private val vectorPreferences: VectorPreferences,
private val stringProvider: StringProvider,
private val activeSessionHolder: ActiveSessionHolder, private val activeSessionHolder: ActiveSessionHolder,
private val iconLoader: IconLoader, private val iconLoader: IconLoader,
private val bitmapLoader: BitmapLoader, private val bitmapLoader: BitmapLoader,
@ -96,7 +98,6 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
notifiableEvent.noisy = false notifiableEvent.noisy = false
eventList.remove(existing) eventList.remove(existing)
eventList.add(notifiableEvent) eventList.add(notifiableEvent)
} else { } else {
//keep the existing one, do not replace //keep the existing one, do not replace
} }
@ -127,6 +128,15 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
} }
} }
fun onEventRedacted(eventId: String) {
synchronized(eventList) {
eventList.filter { it.eventId == eventId }.map { notifiableEvent ->
notifiableEvent.isRedacted = true
notifiableEvent.hasBeenDisplayed = false
}
}
}
/** /**
Clear all known events and refresh the notification drawer Clear all known events and refresh the notification drawer
*/ */
@ -147,7 +157,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
e is NotifiableMessageEvent && e.roomId == roomId e is NotifiableMessageEvent && e.roomId == roomId
} }
} }
NotificationUtils.cancelNotificationMessage(context, roomId, ROOM_MESSAGES_NOTIFICATION_ID) notificationUtils.cancelNotificationMessage(roomId, ROOM_MESSAGES_NOTIFICATION_ID)
} }
refreshNotificationDrawer() refreshNotificationDrawer()
} }
@ -215,20 +225,15 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
val summaryInboxStyle = NotificationCompat.InboxStyle() val summaryInboxStyle = NotificationCompat.InboxStyle()
//group events by room to create a single MessagingStyle notif //group events by room to create a single MessagingStyle notif
val roomIdToEventMap: MutableMap<String, ArrayList<NotifiableMessageEvent>> = HashMap() val roomIdToEventMap: MutableMap<String, MutableList<NotifiableMessageEvent>> = LinkedHashMap()
val simpleEvents: ArrayList<NotifiableEvent> = ArrayList() val simpleEvents: MutableList<NotifiableEvent> = ArrayList()
val notifications: ArrayList<Notification> = ArrayList()
val eventIterator = eventList.listIterator() val eventIterator = eventList.listIterator()
while (eventIterator.hasNext()) { while (eventIterator.hasNext()) {
val event = eventIterator.next() val event = eventIterator.next()
if (event is NotifiableMessageEvent) { if (event is NotifiableMessageEvent) {
val roomId = event.roomId val roomId = event.roomId
var roomEvents = roomIdToEventMap[roomId] val roomEvents = roomIdToEventMap.getOrPut(roomId) { ArrayList() }
if (roomEvents == null) {
roomEvents = ArrayList()
roomIdToEventMap[roomId] = roomEvents
}
if (shouldIgnoreMessageEventInRoom(roomId) || outdatedDetector?.isMessageOutdated(event) == true) { if (shouldIgnoreMessageEventInRoom(roomId) || outdatedDetector?.isMessageOutdated(event) == true) {
//forget this event //forget this event
@ -246,13 +251,13 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
var globalLastMessageTimestamp = 0L var globalLastMessageTimestamp = 0L
//events have been grouped //events have been grouped by roomId
for ((roomId, events) in roomIdToEventMap) { for ((roomId, events) in roomIdToEventMap) {
// Build the notification for the room
if (events.isEmpty()) { if (events.isEmpty() || events.all { it.isRedacted }) {
//Just clear this notification //Just clear this notification
Timber.v("%%%%%%%% REFRESH NOTIFICATION DRAWER $roomId has no more events") Timber.v("%%%%%%%% REFRESH NOTIFICATION DRAWER $roomId has no more events")
NotificationUtils.cancelNotificationMessage(context, roomId, ROOM_MESSAGES_NOTIFICATION_ID) notificationUtils.cancelNotificationMessage(roomId, ROOM_MESSAGES_NOTIFICATION_ID)
continue continue
} }
@ -280,7 +285,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
for (event in events) { for (event in events) {
//if all events in this room have already been displayed there is no need to update it //if all events in this room have already been displayed there is no need to update it
if (!event.hasBeenDisplayed) { if (!event.hasBeenDisplayed && !event.isRedacted) {
roomEventGroupInfo.shouldBing = roomEventGroupInfo.shouldBing || event.noisy roomEventGroupInfo.shouldBing = roomEventGroupInfo.shouldBing || event.noisy
roomEventGroupInfo.customSound = event.soundName roomEventGroupInfo.customSound = event.soundName
} }
@ -293,16 +298,18 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
.build() .build()
if (event.outGoingMessage && event.outGoingMessageFailed) { if (event.outGoingMessage && event.outGoingMessageFailed) {
style.addMessage(context.getString(R.string.notification_inline_reply_failed), event.timestamp, senderPerson) style.addMessage(stringProvider.getString(R.string.notification_inline_reply_failed), event.timestamp, senderPerson)
roomEventGroupInfo.hasSmartReplyError = true roomEventGroupInfo.hasSmartReplyError = true
} else { } else {
style.addMessage(event.body, event.timestamp, senderPerson) if (!event.isRedacted) {
style.addMessage(event.body, event.timestamp, senderPerson)
}
} }
event.hasBeenDisplayed = true //we can consider it as displayed event.hasBeenDisplayed = true //we can consider it as displayed
//It is possible that this event was previously shown as an 'anonymous' simple notif. //It is possible that this event was previously shown as an 'anonymous' simple notif.
//And now it will be merged in a single MessageStyle notif, so we can clean to be sure //And now it will be merged in a single MessageStyle notif, so we can clean to be sure
NotificationUtils.cancelNotificationMessage(context, event.eventId, ROOM_EVENT_NOTIFICATION_ID) notificationUtils.cancelNotificationMessage(event.eventId, ROOM_EVENT_NOTIFICATION_ID)
} }
try { try {
@ -328,7 +335,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
summaryInboxStyle.addLine(line) summaryInboxStyle.addLine(line)
} }
} else { } else {
val summaryLine = context.resources.getQuantityString( val summaryLine = stringProvider.getQuantityString(
R.plurals.notification_compat_summary_line_for_room, events.size, roomName, events.size) R.plurals.notification_compat_summary_line_for_room, events.size, roomName, events.size)
summaryInboxStyle.addLine(summaryLine) summaryInboxStyle.addLine(summaryLine)
} }
@ -347,18 +354,16 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
globalLastMessageTimestamp = lastMessageTimestamp globalLastMessageTimestamp = lastMessageTimestamp
} }
NotificationUtils.buildMessagesListNotification(context, val notification = notificationUtils.buildMessagesListNotification(
vectorPreferences,
style, style,
roomEventGroupInfo, roomEventGroupInfo,
largeBitmap, largeBitmap,
lastMessageTimestamp, lastMessageTimestamp,
myUserDisplayName) myUserDisplayName)
?.let {
//is there an id for this room? //is there an id for this room?
notifications.add(it) notificationUtils.showNotificationMessage(roomId, ROOM_MESSAGES_NOTIFICATION_ID, notification)
NotificationUtils.showNotificationMessage(context, roomId, ROOM_MESSAGES_NOTIFICATION_ID, it)
}
hasNewEvent = true hasNewEvent = true
summaryIsNoisy = summaryIsNoisy || roomEventGroupInfo.shouldBing summaryIsNoisy = summaryIsNoisy || roomEventGroupInfo.shouldBing
} else { } else {
@ -371,14 +376,12 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
for (event in simpleEvents) { for (event in simpleEvents) {
//We build a simple event //We build a simple event
if (firstTime || !event.hasBeenDisplayed) { if (firstTime || !event.hasBeenDisplayed) {
NotificationUtils.buildSimpleEventNotification(context, vectorPreferences, event, null, session.myUserId)?.let { val notification = notificationUtils.buildSimpleEventNotification(event, null, session.myUserId)
notifications.add(it) notificationUtils.showNotificationMessage(event.eventId, ROOM_EVENT_NOTIFICATION_ID, notification)
NotificationUtils.showNotificationMessage(context, event.eventId, ROOM_EVENT_NOTIFICATION_ID, it) event.hasBeenDisplayed = true //we can consider it as displayed
event.hasBeenDisplayed = true //we can consider it as displayed hasNewEvent = true
hasNewEvent = true summaryIsNoisy = summaryIsNoisy || event.noisy
summaryIsNoisy = summaryIsNoisy || event.noisy summaryInboxStyle.addLine(event.description)
summaryInboxStyle.addLine(event.description)
}
} }
} }
@ -396,28 +399,22 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
// To ensure the best experience on all devices and versions, always include a group summary when you create a group // To ensure the best experience on all devices and versions, always include a group summary when you create a group
// https://developer.android.com/training/notify-user/group // https://developer.android.com/training/notify-user/group
if (eventList.isEmpty()) { if (eventList.isEmpty() || eventList.all { it.isRedacted }) {
NotificationUtils.cancelNotificationMessage(context, null, SUMMARY_NOTIFICATION_ID) notificationUtils.cancelNotificationMessage(null, SUMMARY_NOTIFICATION_ID)
} else { } else {
val nbEvents = roomIdToEventMap.size + simpleEvents.size val nbEvents = roomIdToEventMap.size + simpleEvents.size
val sumTitle = context.resources.getQuantityString( val sumTitle = stringProvider.getQuantityString(R.plurals.notification_compat_summary_title, nbEvents, nbEvents)
R.plurals.notification_compat_summary_title, nbEvents, nbEvents)
summaryInboxStyle.setBigContentTitle(sumTitle) summaryInboxStyle.setBigContentTitle(sumTitle)
//TODO get latest event? //TODO get latest event?
.setSummaryText( .setSummaryText(stringProvider.getQuantityString(R.plurals.notification_unread_notified_messages, nbEvents, nbEvents))
context.resources
.getQuantityString(R.plurals.notification_unread_notified_messages, nbEvents, nbEvents))
NotificationUtils.buildSummaryListNotification( val notification = notificationUtils.buildSummaryListNotification(
context,
vectorPreferences,
summaryInboxStyle, summaryInboxStyle,
sumTitle, sumTitle,
noisy = hasNewEvent && summaryIsNoisy, noisy = hasNewEvent && summaryIsNoisy,
lastMessageTimestamp = globalLastMessageTimestamp lastMessageTimestamp = globalLastMessageTimestamp)
)?.let {
NotificationUtils.showNotificationMessage(context, null, SUMMARY_NOTIFICATION_ID, it) notificationUtils.showNotificationMessage(null, SUMMARY_NOTIFICATION_ID, notification)
}
if (hasNewEvent && summaryIsNoisy) { if (hasNewEvent && summaryIsNoisy) {
try { try {
@ -443,12 +440,11 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
} }
} }
private fun getRoomBitmap(events: ArrayList<NotifiableMessageEvent>): Bitmap? { private fun getRoomBitmap(events: List<NotifiableMessageEvent>): Bitmap? {
if (events.isEmpty()) return null if (events.isEmpty()) return null
//Use the last event (most recent?) //Use the last event (most recent?)
val roomAvatarPath = events.last().roomAvatarPath val roomAvatarPath = events.last().roomAvatarPath ?: events.last().senderAvatarPath
?: events.last().senderAvatarPath
return bitmapLoader.getRoomBitmap(roomAvatarPath) return bitmapLoader.getRoomBitmap(roomAvatarPath)
} }
@ -461,7 +457,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
fun persistInfo() { fun persistInfo() {
synchronized(eventList) { synchronized(eventList) {
if (eventList.isEmpty()) { if (eventList.isEmpty()) {
deleteCachedRoomNotifications(context) deleteCachedRoomNotifications()
return return
} }
try { try {
@ -476,14 +472,14 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
} }
} }
private fun loadEventInfo(): ArrayList<NotifiableEvent> { private fun loadEventInfo(): MutableList<NotifiableEvent> {
try { try {
val file = File(context.applicationContext.cacheDir, ROOMS_NOTIFICATIONS_FILE_NAME) val file = File(context.applicationContext.cacheDir, ROOMS_NOTIFICATIONS_FILE_NAME)
if (file.exists()) { if (file.exists()) {
FileInputStream(file).use { FileInputStream(file).use {
val events: ArrayList<NotifiableEvent>? = activeSessionHolder.getSafeActiveSession()?.loadSecureSecret(it, KEY_ALIAS_SECRET_STORAGE) val events: ArrayList<NotifiableEvent>? = activeSessionHolder.getSafeActiveSession()?.loadSecureSecret(it, KEY_ALIAS_SECRET_STORAGE)
if (events != null) { if (events != null) {
return ArrayList(events.mapNotNull { it as? NotifiableEvent }) return events.toMutableList()
} }
} }
} }
@ -493,7 +489,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
return ArrayList() return ArrayList()
} }
private fun deleteCachedRoomNotifications(context: Context) { private fun deleteCachedRoomNotifications() {
val file = File(context.applicationContext.cacheDir, ROOMS_NOTIFICATIONS_FILE_NAME) val file = File(context.applicationContext.cacheDir, ROOMS_NOTIFICATIONS_FILE_NAME)
if (file.exists()) { if (file.exists()) {
file.delete() file.delete()

View File

@ -27,7 +27,6 @@ import android.content.Intent
import android.graphics.Bitmap import android.graphics.Bitmap
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.text.TextUtils
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
@ -37,6 +36,7 @@ import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import im.vector.riotx.BuildConfig import im.vector.riotx.BuildConfig
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.core.utils.startNotificationChannelSettingsIntent import im.vector.riotx.core.utils.startNotificationChannelSettingsIntent
import im.vector.riotx.features.home.HomeActivity import im.vector.riotx.features.home.HomeActivity
import im.vector.riotx.features.home.room.detail.RoomDetailActivity import im.vector.riotx.features.home.room.detail.RoomDetailActivity
@ -44,51 +44,73 @@ import im.vector.riotx.features.home.room.detail.RoomDetailArgs
import im.vector.riotx.features.settings.VectorPreferences import im.vector.riotx.features.settings.VectorPreferences
import timber.log.Timber import timber.log.Timber
import java.util.* import java.util.*
import javax.inject.Inject
import javax.inject.Singleton
fun supportNotificationChannels() = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
/** /**
* Util class for creating notifications. * Util class for creating notifications.
* Note: Cannot inject ColorProvider in the constructor, because it requires an Activity
*/ */
object NotificationUtils { @Singleton
class NotificationUtils @Inject constructor(private val context: Context,
private val stringProvider: StringProvider,
private val vectorPreferences: VectorPreferences) {
/* ========================================================================================== companion object {
* IDs for notifications /* ==========================================================================================
* ========================================================================================== */ * IDs for notifications
* ========================================================================================== */
/** /**
* Identifier of the foreground notification used to keep the application alive * Identifier of the foreground notification used to keep the application alive
* when it runs in background. * when it runs in background.
* This notification, which is not removable by the end user, displays what * This notification, which is not removable by the end user, displays what
* the application is doing while in background. * the application is doing while in background.
*/ */
const val NOTIFICATION_ID_FOREGROUND_SERVICE = 61 const val NOTIFICATION_ID_FOREGROUND_SERVICE = 61
/* ========================================================================================== /* ==========================================================================================
* IDs for actions * IDs for actions
* ========================================================================================== */ * ========================================================================================== */
const val JOIN_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.JOIN_ACTION" const val JOIN_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.JOIN_ACTION"
const val REJECT_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.REJECT_ACTION" const val REJECT_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.REJECT_ACTION"
private const val QUICK_LAUNCH_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.QUICK_LAUNCH_ACTION" private const val QUICK_LAUNCH_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.QUICK_LAUNCH_ACTION"
const val MARK_ROOM_READ_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.MARK_ROOM_READ_ACTION" const val MARK_ROOM_READ_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.MARK_ROOM_READ_ACTION"
const val SMART_REPLY_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.SMART_REPLY_ACTION" const val SMART_REPLY_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.SMART_REPLY_ACTION"
const val DISMISS_SUMMARY_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.DISMISS_SUMMARY_ACTION" const val DISMISS_SUMMARY_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.DISMISS_SUMMARY_ACTION"
const val DISMISS_ROOM_NOTIF_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.DISMISS_ROOM_NOTIF_ACTION" const val DISMISS_ROOM_NOTIF_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.DISMISS_ROOM_NOTIF_ACTION"
private const val TAP_TO_VIEW_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.TAP_TO_VIEW_ACTION" private const val TAP_TO_VIEW_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.TAP_TO_VIEW_ACTION"
/* ========================================================================================== /* ==========================================================================================
* IDs for channels * IDs for channels
* ========================================================================================== */ * ========================================================================================== */
// on devices >= android O, we need to define a channel for each notifications // on devices >= android O, we need to define a channel for each notifications
private const val LISTENING_FOR_EVENTS_NOTIFICATION_CHANNEL_ID = "LISTEN_FOR_EVENTS_NOTIFICATION_CHANNEL_ID" private const val LISTENING_FOR_EVENTS_NOTIFICATION_CHANNEL_ID = "LISTEN_FOR_EVENTS_NOTIFICATION_CHANNEL_ID"
private const val NOISY_NOTIFICATION_CHANNEL_ID = "DEFAULT_NOISY_NOTIFICATION_CHANNEL_ID" private const val NOISY_NOTIFICATION_CHANNEL_ID = "DEFAULT_NOISY_NOTIFICATION_CHANNEL_ID"
private const val SILENT_NOTIFICATION_CHANNEL_ID = "DEFAULT_SILENT_NOTIFICATION_CHANNEL_ID_V2" private const val SILENT_NOTIFICATION_CHANNEL_ID = "DEFAULT_SILENT_NOTIFICATION_CHANNEL_ID_V2"
private const val CALL_NOTIFICATION_CHANNEL_ID = "CALL_NOTIFICATION_CHANNEL_ID_V2" private const val CALL_NOTIFICATION_CHANNEL_ID = "CALL_NOTIFICATION_CHANNEL_ID_V2"
fun supportNotificationChannels() = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
fun openSystemSettingsForSilentCategory(fragment: Fragment) {
startNotificationChannelSettingsIntent(fragment, SILENT_NOTIFICATION_CHANNEL_ID)
}
fun openSystemSettingsForNoisyCategory(fragment: Fragment) {
startNotificationChannelSettingsIntent(fragment, NOISY_NOTIFICATION_CHANNEL_ID)
}
fun openSystemSettingsForCallCategory(fragment: Fragment) {
startNotificationChannelSettingsIntent(fragment, CALL_NOTIFICATION_CHANNEL_ID)
}
}
private val notificationManager = NotificationManagerCompat.from(context)
/* ========================================================================================== /* ==========================================================================================
* Channel names * Channel names
@ -96,17 +118,13 @@ object NotificationUtils {
/** /**
* Create notification channels. * Create notification channels.
*
* @param context the context
*/ */
@TargetApi(Build.VERSION_CODES.O) @TargetApi(Build.VERSION_CODES.O)
fun createNotificationChannels(context: Context) { fun createNotificationChannels() {
if (!supportNotificationChannels()) { if (!supportNotificationChannels()) {
return return
} }
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color) val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color)
//Migration - the noisy channel was deleted and recreated when sound preference was changed (id was DEFAULT_NOISY_NOTIFICATION_CHANNEL_ID_BASE //Migration - the noisy channel was deleted and recreated when sound preference was changed (id was DEFAULT_NOISY_NOTIFICATION_CHANNEL_ID_BASE
@ -132,10 +150,10 @@ object NotificationUtils {
* intrude. * intrude.
*/ */
notificationManager.createNotificationChannel(NotificationChannel(NOISY_NOTIFICATION_CHANNEL_ID, notificationManager.createNotificationChannel(NotificationChannel(NOISY_NOTIFICATION_CHANNEL_ID,
context.getString(R.string.notification_noisy_notifications), stringProvider.getString(R.string.notification_noisy_notifications),
NotificationManager.IMPORTANCE_DEFAULT) NotificationManager.IMPORTANCE_DEFAULT)
.apply { .apply {
description = context.getString(R.string.notification_noisy_notifications) description = stringProvider.getString(R.string.notification_noisy_notifications)
enableVibration(true) enableVibration(true)
enableLights(true) enableLights(true)
lightColor = accentColor lightColor = accentColor
@ -145,29 +163,29 @@ object NotificationUtils {
* Low notification importance: shows everywhere, but is not intrusive. * Low notification importance: shows everywhere, but is not intrusive.
*/ */
notificationManager.createNotificationChannel(NotificationChannel(SILENT_NOTIFICATION_CHANNEL_ID, notificationManager.createNotificationChannel(NotificationChannel(SILENT_NOTIFICATION_CHANNEL_ID,
context.getString(R.string.notification_silent_notifications), stringProvider.getString(R.string.notification_silent_notifications),
NotificationManager.IMPORTANCE_LOW) NotificationManager.IMPORTANCE_LOW)
.apply { .apply {
description = context.getString(R.string.notification_silent_notifications) description = stringProvider.getString(R.string.notification_silent_notifications)
setSound(null, null) setSound(null, null)
enableLights(true) enableLights(true)
lightColor = accentColor lightColor = accentColor
}) })
notificationManager.createNotificationChannel(NotificationChannel(LISTENING_FOR_EVENTS_NOTIFICATION_CHANNEL_ID, notificationManager.createNotificationChannel(NotificationChannel(LISTENING_FOR_EVENTS_NOTIFICATION_CHANNEL_ID,
context.getString(R.string.notification_listening_for_events), stringProvider.getString(R.string.notification_listening_for_events),
NotificationManager.IMPORTANCE_MIN) NotificationManager.IMPORTANCE_MIN)
.apply { .apply {
description = context.getString(R.string.notification_listening_for_events) description = stringProvider.getString(R.string.notification_listening_for_events)
setSound(null, null) setSound(null, null)
setShowBadge(false) setShowBadge(false)
}) })
notificationManager.createNotificationChannel(NotificationChannel(CALL_NOTIFICATION_CHANNEL_ID, notificationManager.createNotificationChannel(NotificationChannel(CALL_NOTIFICATION_CHANNEL_ID,
context.getString(R.string.call), stringProvider.getString(R.string.call),
NotificationManager.IMPORTANCE_HIGH) NotificationManager.IMPORTANCE_HIGH)
.apply { .apply {
description = context.getString(R.string.call) description = stringProvider.getString(R.string.call)
setSound(null, null) setSound(null, null)
enableLights(true) enableLights(true)
lightColor = accentColor lightColor = accentColor
@ -177,12 +195,11 @@ object NotificationUtils {
/** /**
* Build a polling thread listener notification * Build a polling thread listener notification
* *
* @param context Android context
* @param subTitleResId subtitle string resource Id of the notification * @param subTitleResId subtitle string resource Id of the notification
* @return the polling thread listener notification * @return the polling thread listener notification
*/ */
@SuppressLint("NewApi") @SuppressLint("NewApi")
fun buildForegroundServiceNotification(context: Context, @StringRes subTitleResId: Int, withProgress: Boolean = true): Notification { fun buildForegroundServiceNotification(@StringRes subTitleResId: Int, withProgress: Boolean = true): Notification {
// build the pending intent go to the home screen if this is clicked. // build the pending intent go to the home screen if this is clicked.
val i = Intent(context, HomeActivity::class.java) val i = Intent(context, HomeActivity::class.java)
i.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP i.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
@ -191,7 +208,7 @@ object NotificationUtils {
val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color) val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color)
val builder = NotificationCompat.Builder(context, LISTENING_FOR_EVENTS_NOTIFICATION_CHANNEL_ID) val builder = NotificationCompat.Builder(context, LISTENING_FOR_EVENTS_NOTIFICATION_CHANNEL_ID)
.setContentTitle(context.getString(subTitleResId)) .setContentTitle(stringProvider.getString(subTitleResId))
.setSmallIcon(R.drawable.sync) .setSmallIcon(R.drawable.sync)
.setCategory(NotificationCompat.CATEGORY_SERVICE) .setCategory(NotificationCompat.CATEGORY_SERVICE)
.setColor(accentColor) .setColor(accentColor)
@ -225,7 +242,7 @@ object NotificationUtils {
CharSequence::class.java, CharSequence::class.java,
CharSequence::class.java, CharSequence::class.java,
PendingIntent::class.java) PendingIntent::class.java)
deprecatedMethod.invoke(notification, context, context.getString(R.string.app_name), context.getString(subTitleResId), pi) deprecatedMethod.invoke(notification, context, stringProvider.getString(R.string.app_name), stringProvider.getString(subTitleResId), pi)
} catch (ex: Exception) { } catch (ex: Exception) {
Timber.e(ex, "## buildNotification(): Exception - setLatestEventInfo() Msg=") Timber.e(ex, "## buildNotification(): Exception - setLatestEventInfo() Msg=")
} }
@ -238,7 +255,6 @@ object NotificationUtils {
* Build an incoming call notification. * Build an incoming call notification.
* This notification starts the VectorHomeActivity which is in charge of centralizing the incoming call flow. * This notification starts the VectorHomeActivity which is in charge of centralizing the incoming call flow.
* *
* @param context the context.
* @param isVideo true if this is a video call, false for voice call * @param isVideo true if this is a video call, false for voice call
* @param roomName the room name in which the call is pending. * @param roomName the room name in which the call is pending.
* @param matrixId the matrix id * @param matrixId the matrix id
@ -246,20 +262,19 @@ object NotificationUtils {
* @return the call notification. * @return the call notification.
*/ */
@SuppressLint("NewApi") @SuppressLint("NewApi")
fun buildIncomingCallNotification(context: Context, fun buildIncomingCallNotification(isVideo: Boolean,
isVideo: Boolean,
roomName: String, roomName: String,
matrixId: String, matrixId: String,
callId: String): Notification { callId: String): Notification {
val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color) val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color)
val builder = NotificationCompat.Builder(context, CALL_NOTIFICATION_CHANNEL_ID) val builder = NotificationCompat.Builder(context, CALL_NOTIFICATION_CHANNEL_ID)
.setContentTitle(ensureTitleNotEmpty(context, roomName)) .setContentTitle(ensureTitleNotEmpty(roomName))
.apply { .apply {
if (isVideo) { if (isVideo) {
setContentText(context.getString(R.string.incoming_video_call)) setContentText(stringProvider.getString(R.string.incoming_video_call))
} else { } else {
setContentText(context.getString(R.string.incoming_voice_call)) setContentText(stringProvider.getString(R.string.incoming_voice_call))
} }
} }
.setSmallIcon(R.drawable.incoming_call_notification_transparent) .setSmallIcon(R.drawable.incoming_call_notification_transparent)
@ -295,7 +310,6 @@ object NotificationUtils {
/** /**
* Build a pending call notification * Build a pending call notification
* *
* @param context the context.
* @param isVideo true if this is a video call, false for voice call * @param isVideo true if this is a video call, false for voice call
* @param roomName the room name in which the call is pending. * @param roomName the room name in which the call is pending.
* @param roomId the room Id * @param roomId the room Id
@ -304,20 +318,19 @@ object NotificationUtils {
* @return the call notification. * @return the call notification.
*/ */
@SuppressLint("NewApi") @SuppressLint("NewApi")
fun buildPendingCallNotification(context: Context, fun buildPendingCallNotification(isVideo: Boolean,
isVideo: Boolean,
roomName: String, roomName: String,
roomId: String, roomId: String,
matrixId: String, matrixId: String,
callId: String): Notification { callId: String): Notification {
val builder = NotificationCompat.Builder(context, CALL_NOTIFICATION_CHANNEL_ID) val builder = NotificationCompat.Builder(context, CALL_NOTIFICATION_CHANNEL_ID)
.setContentTitle(ensureTitleNotEmpty(context, roomName)) .setContentTitle(ensureTitleNotEmpty(roomName))
.apply { .apply {
if (isVideo) { if (isVideo) {
setContentText(context.getString(R.string.video_call_in_progress)) setContentText(stringProvider.getString(R.string.video_call_in_progress))
} else { } else {
setContentText(context.getString(R.string.call_in_progress)) setContentText(stringProvider.getString(R.string.call_in_progress))
} }
} }
.setSmallIcon(R.drawable.incoming_call_notification_transparent) .setSmallIcon(R.drawable.incoming_call_notification_transparent)
@ -355,9 +368,9 @@ object NotificationUtils {
/** /**
* Build a temporary (because service will be stopped just after) notification for the CallService, when a call is ended * Build a temporary (because service will be stopped just after) notification for the CallService, when a call is ended
*/ */
fun buildCallEndedNotification(context: Context): Notification { fun buildCallEndedNotification(): Notification {
return NotificationCompat.Builder(context, CALL_NOTIFICATION_CHANNEL_ID) return NotificationCompat.Builder(context, CALL_NOTIFICATION_CHANNEL_ID)
.setContentTitle(context.getString(R.string.call_ended)) .setContentTitle(stringProvider.getString(R.string.call_ended))
.setSmallIcon(R.drawable.ic_material_call_end_grey) .setSmallIcon(R.drawable.ic_material_call_end_grey)
.setCategory(NotificationCompat.CATEGORY_CALL) .setCategory(NotificationCompat.CATEGORY_CALL)
.build() .build()
@ -366,17 +379,14 @@ object NotificationUtils {
/** /**
* Build a notification for a Room * Build a notification for a Room
*/ */
fun buildMessagesListNotification(context: Context, fun buildMessagesListNotification(messageStyle: NotificationCompat.MessagingStyle,
vectorPreferences: VectorPreferences,
messageStyle: NotificationCompat.MessagingStyle,
roomInfo: RoomEventGroupInfo, roomInfo: RoomEventGroupInfo,
largeIcon: Bitmap?, largeIcon: Bitmap?,
lastMessageTimestamp: Long, lastMessageTimestamp: Long,
senderDisplayNameForReplyCompat: String?): Notification? { senderDisplayNameForReplyCompat: String?): Notification {
val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color) val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color)
// Build the pending intent for when the notification is clicked // Build the pending intent for when the notification is clicked
val openRoomIntent = buildOpenRoomIntent(context, roomInfo.roomId) val openRoomIntent = buildOpenRoomIntent(roomInfo.roomId)
val smallIcon = R.drawable.ic_status_bar val smallIcon = R.drawable.ic_status_bar
val channelID = if (roomInfo.shouldBing) NOISY_NOTIFICATION_CHANNEL_ID else SILENT_NOTIFICATION_CHANNEL_ID val channelID = if (roomInfo.shouldBing) NOISY_NOTIFICATION_CHANNEL_ID else SILENT_NOTIFICATION_CHANNEL_ID
@ -393,18 +403,15 @@ object NotificationUtils {
// Title for API < 16 devices. // Title for API < 16 devices.
.setContentTitle(roomInfo.roomDisplayName) .setContentTitle(roomInfo.roomDisplayName)
// Content for API < 16 devices. // Content for API < 16 devices.
.setContentText(context.getString(R.string.notification_new_messages)) .setContentText(stringProvider.getString(R.string.notification_new_messages))
// Number of new notifications for API <24 (M and below) devices. // Number of new notifications for API <24 (M and below) devices.
.setSubText(context .setSubText(stringProvider.getQuantityString(R.plurals.room_new_messages_notification, messageStyle.messages.size, messageStyle.messages.size))
.resources
.getQuantityString(R.plurals.room_new_messages_notification, messageStyle.messages.size, messageStyle.messages.size)
)
// Auto-bundling is enabled for 4 or more notifications on API 24+ (N+) // 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 // devices and all Wear devices. But we want a custom grouping, so we specify the groupID
// TODO Group should be current user display name // TODO Group should be current user display name
.setGroup(context.getString(R.string.app_name)) .setGroup(stringProvider.getString(R.string.app_name))
//In order to avoid notification making sound twice (due to the summary notification) //In order to avoid notification making sound twice (due to the summary notification)
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY) .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY)
@ -440,17 +447,17 @@ object NotificationUtils {
addAction(NotificationCompat.Action( addAction(NotificationCompat.Action(
R.drawable.ic_material_done_all_white, R.drawable.ic_material_done_all_white,
context.getString(R.string.action_mark_room_read), stringProvider.getString(R.string.action_mark_room_read),
markRoomReadPendingIntent)) markRoomReadPendingIntent))
// Quick reply // Quick reply
if (!roomInfo.hasSmartReplyError) { if (!roomInfo.hasSmartReplyError) {
buildQuickReplyIntent(context, roomInfo.roomId, senderDisplayNameForReplyCompat)?.let { replyPendingIntent -> buildQuickReplyIntent(roomInfo.roomId, senderDisplayNameForReplyCompat)?.let { replyPendingIntent ->
val remoteInput = RemoteInput.Builder(NotificationBroadcastReceiver.KEY_TEXT_REPLY) val remoteInput = RemoteInput.Builder(NotificationBroadcastReceiver.KEY_TEXT_REPLY)
.setLabel(context.getString(R.string.action_quick_reply)) .setLabel(stringProvider.getString(R.string.action_quick_reply))
.build() .build()
NotificationCompat.Action.Builder(R.drawable.vector_notification_quick_reply, NotificationCompat.Action.Builder(R.drawable.vector_notification_quick_reply,
context.getString(R.string.action_quick_reply), replyPendingIntent) stringProvider.getString(R.string.action_quick_reply), replyPendingIntent)
.addRemoteInput(remoteInput) .addRemoteInput(remoteInput)
.build()?.let { .build()?.let {
addAction(it) addAction(it)
@ -477,11 +484,9 @@ object NotificationUtils {
} }
fun buildSimpleEventNotification(context: Context, fun buildSimpleEventNotification(simpleNotifiableEvent: NotifiableEvent,
vectorPreferences: VectorPreferences,
simpleNotifiableEvent: NotifiableEvent,
largeIcon: Bitmap?, largeIcon: Bitmap?,
matrixId: String): Notification? { matrixId: String): Notification {
val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color) val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color)
// Build the pending intent for when the notification is clicked // Build the pending intent for when the notification is clicked
val smallIcon = R.drawable.ic_status_bar val smallIcon = R.drawable.ic_status_bar
@ -489,9 +494,9 @@ object NotificationUtils {
val channelID = if (simpleNotifiableEvent.noisy) NOISY_NOTIFICATION_CHANNEL_ID else SILENT_NOTIFICATION_CHANNEL_ID val channelID = if (simpleNotifiableEvent.noisy) NOISY_NOTIFICATION_CHANNEL_ID else SILENT_NOTIFICATION_CHANNEL_ID
return NotificationCompat.Builder(context, channelID) return NotificationCompat.Builder(context, channelID)
.setContentTitle(context.getString(R.string.app_name)) .setContentTitle(stringProvider.getString(R.string.app_name))
.setContentText(simpleNotifiableEvent.description) .setContentText(simpleNotifiableEvent.description)
.setGroup(context.getString(R.string.app_name)) .setGroup(stringProvider.getString(R.string.app_name))
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY) .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY)
.setSmallIcon(smallIcon) .setSmallIcon(smallIcon)
.setColor(accentColor) .setColor(accentColor)
@ -508,7 +513,7 @@ object NotificationUtils {
addAction( addAction(
R.drawable.vector_notification_reject_invitation, R.drawable.vector_notification_reject_invitation,
context.getString(R.string.reject), stringProvider.getString(R.string.reject),
rejectIntentPendingIntent) rejectIntentPendingIntent)
// offer to type a quick accept button // offer to type a quick accept button
@ -520,7 +525,7 @@ object NotificationUtils {
PendingIntent.FLAG_UPDATE_CURRENT) PendingIntent.FLAG_UPDATE_CURRENT)
addAction( addAction(
R.drawable.vector_notification_accept_invitation, R.drawable.vector_notification_accept_invitation,
context.getString(R.string.join), stringProvider.getString(R.string.join),
joinIntentPendingIntent) joinIntentPendingIntent)
} else { } else {
setAutoCancel(true) setAutoCancel(true)
@ -551,7 +556,7 @@ object NotificationUtils {
.build() .build()
} }
private fun buildOpenRoomIntent(context: Context, roomId: String): PendingIntent? { private fun buildOpenRoomIntent(roomId: String): PendingIntent? {
val roomIntentTap = RoomDetailActivity.newIntent(context, RoomDetailArgs(roomId)) val roomIntentTap = RoomDetailActivity.newIntent(context, RoomDetailArgs(roomId))
roomIntentTap.action = TAP_TO_VIEW_ACTION roomIntentTap.action = TAP_TO_VIEW_ACTION
//pending intent get reused by system, this will mess up the extra params, so put unique info to avoid that //pending intent get reused by system, this will mess up the extra params, so put unique info to avoid that
@ -564,7 +569,7 @@ object NotificationUtils {
.getPendingIntent(System.currentTimeMillis().toInt(), PendingIntent.FLAG_UPDATE_CURRENT) .getPendingIntent(System.currentTimeMillis().toInt(), PendingIntent.FLAG_UPDATE_CURRENT)
} }
private fun buildOpenHomePendingIntentForSummary(context: Context): PendingIntent { private fun buildOpenHomePendingIntentForSummary(): PendingIntent {
val intent = HomeActivity.newIntent(context, clearNotification = true) val intent = HomeActivity.newIntent(context, clearNotification = true)
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
intent.data = Uri.parse("foobar://tapSummary") intent.data = Uri.parse("foobar://tapSummary")
@ -578,7 +583,7 @@ object NotificationUtils {
However, for Android devices running Marshmallow and below (API level 23 and below), However, for Android devices running Marshmallow and below (API level 23 and below),
it will be more appropriate to use an activity. Since you have to provide your own UI. it will be more appropriate to use an activity. Since you have to provide your own UI.
*/ */
private fun buildQuickReplyIntent(context: Context, roomId: String, senderName: String?): PendingIntent? { private fun buildQuickReplyIntent(roomId: String, senderName: String?): PendingIntent? {
val intent: Intent val intent: Intent
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
intent = Intent(context, NotificationBroadcastReceiver::class.java) intent = Intent(context, NotificationBroadcastReceiver::class.java)
@ -610,12 +615,10 @@ object NotificationUtils {
/** /**
* Build the summary notification * Build the summary notification
*/ */
fun buildSummaryListNotification(context: Context, fun buildSummaryListNotification(style: NotificationCompat.InboxStyle,
vectorPreferences: VectorPreferences,
style: NotificationCompat.InboxStyle,
compatSummary: String, compatSummary: String,
noisy: Boolean, noisy: Boolean,
lastMessageTimestamp: Long): Notification? { lastMessageTimestamp: Long): Notification {
val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color) val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color)
val smallIcon = R.drawable.ic_status_bar val smallIcon = R.drawable.ic_status_bar
@ -623,12 +626,12 @@ object NotificationUtils {
// used in compat < N, after summary is built based on child notifications // used in compat < N, after summary is built based on child notifications
.setWhen(lastMessageTimestamp) .setWhen(lastMessageTimestamp)
.setStyle(style) .setStyle(style)
.setContentTitle(context.getString(R.string.app_name)) .setContentTitle(stringProvider.getString(R.string.app_name))
.setCategory(NotificationCompat.CATEGORY_MESSAGE) .setCategory(NotificationCompat.CATEGORY_MESSAGE)
.setSmallIcon(smallIcon) .setSmallIcon(smallIcon)
//set content text to support devices running API level < 24 //set content text to support devices running API level < 24
.setContentText(compatSummary) .setContentText(compatSummary)
.setGroup(context.getString(R.string.app_name)) .setGroup(stringProvider.getString(R.string.app_name))
//set this notification as the summary for the group //set this notification as the summary for the group
.setGroupSummary(true) .setGroupSummary(true)
.setColor(accentColor) .setColor(accentColor)
@ -645,13 +648,12 @@ object NotificationUtils {
priority = NotificationCompat.PRIORITY_LOW priority = NotificationCompat.PRIORITY_LOW
} }
} }
.setContentIntent(buildOpenHomePendingIntentForSummary(context)) .setContentIntent(buildOpenHomePendingIntentForSummary())
.setDeleteIntent(getDismissSummaryPendingIntent(context)) .setDeleteIntent(getDismissSummaryPendingIntent())
.build() .build()
} }
private fun getDismissSummaryPendingIntent(context: Context): PendingIntent { private fun getDismissSummaryPendingIntent(): PendingIntent {
val intent = Intent(context, NotificationBroadcastReceiver::class.java) val intent = Intent(context, NotificationBroadcastReceiver::class.java)
intent.action = DISMISS_SUMMARY_ACTION intent.action = DISMISS_SUMMARY_ACTION
intent.data = Uri.parse("foobar://deleteSummary") intent.data = Uri.parse("foobar://deleteSummary")
@ -659,33 +661,28 @@ object NotificationUtils {
0, intent, PendingIntent.FLAG_UPDATE_CURRENT) 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
} }
fun showNotificationMessage(context: Context, tag: String?, id: Int, notification: Notification) { fun showNotificationMessage(tag: String?, id: Int, notification: Notification) {
with(NotificationManagerCompat.from(context)) { notificationManager.notify(tag, id, notification)
notify(tag, id, notification)
}
} }
fun cancelNotificationMessage(context: Context, tag: String?, id: Int) { fun cancelNotificationMessage(tag: String?, id: Int) {
NotificationManagerCompat.from(context) notificationManager.cancel(tag, id)
.cancel(tag, id)
} }
/** /**
* Cancel the foreground notification service * Cancel the foreground notification service
*/ */
fun cancelNotificationForegroundService(context: Context) { fun cancelNotificationForegroundService() {
NotificationManagerCompat.from(context) notificationManager.cancel(NOTIFICATION_ID_FOREGROUND_SERVICE)
.cancel(NOTIFICATION_ID_FOREGROUND_SERVICE)
} }
/** /**
* Cancel all the notification * Cancel all the notification
*/ */
fun cancelAllNotifications(context: Context) { fun cancelAllNotifications() {
// Keep this try catch (reported by GA) // Keep this try catch (reported by GA)
try { try {
NotificationManagerCompat.from(context) notificationManager.cancelAll()
.cancelAll()
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e, "## cancelAllNotifications() failed " + e.message) Timber.e(e, "## cancelAllNotifications() failed " + e.message)
} }
@ -694,7 +691,7 @@ object NotificationUtils {
/** /**
* Return true it the user has enabled the do not disturb mode * Return true it the user has enabled the do not disturb mode
*/ */
fun isDoNotDisturbModeOn(context: Context): Boolean { fun isDoNotDisturbModeOn(): Boolean {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
return false return false
} }
@ -706,23 +703,11 @@ object NotificationUtils {
|| setting == NotificationManager.INTERRUPTION_FILTER_ALARMS || setting == NotificationManager.INTERRUPTION_FILTER_ALARMS
} }
private fun ensureTitleNotEmpty(context: Context, title: String?): CharSequence { private fun ensureTitleNotEmpty(title: String?): CharSequence {
if (TextUtils.isEmpty(title)) { if (title.isNullOrBlank()) {
return context.getString(R.string.app_name) return stringProvider.getString(R.string.app_name)
} }
return title!! return title
}
fun openSystemSettingsForSilentCategory(fragment: Fragment) {
startNotificationChannelSettingsIntent(fragment, SILENT_NOTIFICATION_CHANNEL_ID)
}
fun openSystemSettingsForNoisyCategory(fragment: Fragment) {
startNotificationChannelSettingsIntent(fragment, NOISY_NOTIFICATION_CHANNEL_ID)
}
fun openSystemSettingsForCallCategory(fragment: Fragment) {
startNotificationChannelSettingsIntent(fragment, CALL_NOTIFICATION_CHANNEL_ID)
} }
} }

View File

@ -60,6 +60,10 @@ class PushRuleTriggerListener @Inject constructor(
notificationDrawerManager.clearMessageEventOfRoom(roomId) notificationDrawerManager.clearMessageEventOfRoom(roomId)
} }
override fun onEventRedacted(redactedEventId: String) {
notificationDrawerManager.onEventRedacted(redactedEventId)
}
override fun batchFinish() { override fun batchFinish() {
notificationDrawerManager.refreshNotificationDrawer() notificationDrawerManager.refreshNotificationDrawer()
} }

View File

@ -30,6 +30,7 @@ data class SimpleNotifiableEvent(
override var isPushGatewayEvent: Boolean = false) : NotifiableEvent { override var isPushGatewayEvent: Boolean = false) : NotifiableEvent {
override var hasBeenDisplayed: Boolean = false override var hasBeenDisplayed: Boolean = false
override var isRedacted: Boolean = false
override var lockScreenVisibility = NotificationCompat.VISIBILITY_PUBLIC override var lockScreenVisibility = NotificationCompat.VISIBILITY_PUBLIC
} }

View File

@ -29,7 +29,6 @@ import im.vector.riotx.core.extensions.withArgs
import im.vector.riotx.core.preference.BingRule import im.vector.riotx.core.preference.BingRule
import im.vector.riotx.core.preference.BingRulePreference import im.vector.riotx.core.preference.BingRulePreference
import im.vector.riotx.features.notifications.NotificationUtils import im.vector.riotx.features.notifications.NotificationUtils
import im.vector.riotx.features.notifications.supportNotificationChannels
import javax.inject.Inject import javax.inject.Inject
class VectorSettingsAdvancedNotificationPreferenceFragment : VectorSettingsBaseFragment() { class VectorSettingsAdvancedNotificationPreferenceFragment : VectorSettingsBaseFragment() {
@ -56,7 +55,7 @@ class VectorSettingsAdvancedNotificationPreferenceFragment : VectorSettingsBaseF
override fun bindPref() { override fun bindPref() {
val callNotificationsSystemOptions = findPreference(VectorPreferences.SETTINGS_SYSTEM_CALL_NOTIFICATION_PREFERENCE_KEY) val callNotificationsSystemOptions = findPreference(VectorPreferences.SETTINGS_SYSTEM_CALL_NOTIFICATION_PREFERENCE_KEY)
if (supportNotificationChannels()) { if (NotificationUtils.supportNotificationChannels()) {
callNotificationsSystemOptions.onPreferenceClickListener = Preference.OnPreferenceClickListener { callNotificationsSystemOptions.onPreferenceClickListener = Preference.OnPreferenceClickListener {
NotificationUtils.openSystemSettingsForCallCategory(this) NotificationUtils.openSystemSettingsForCallCategory(this)
false false
@ -66,7 +65,7 @@ class VectorSettingsAdvancedNotificationPreferenceFragment : VectorSettingsBaseF
} }
val noisyNotificationsSystemOptions = findPreference(VectorPreferences.SETTINGS_SYSTEM_NOISY_NOTIFICATION_PREFERENCE_KEY) val noisyNotificationsSystemOptions = findPreference(VectorPreferences.SETTINGS_SYSTEM_NOISY_NOTIFICATION_PREFERENCE_KEY)
if (supportNotificationChannels()) { if (NotificationUtils.supportNotificationChannels()) {
noisyNotificationsSystemOptions.onPreferenceClickListener = Preference.OnPreferenceClickListener { noisyNotificationsSystemOptions.onPreferenceClickListener = Preference.OnPreferenceClickListener {
NotificationUtils.openSystemSettingsForNoisyCategory(this) NotificationUtils.openSystemSettingsForNoisyCategory(this)
false false
@ -76,7 +75,7 @@ class VectorSettingsAdvancedNotificationPreferenceFragment : VectorSettingsBaseF
} }
val silentNotificationsSystemOptions = findPreference(VectorPreferences.SETTINGS_SYSTEM_SILENT_NOTIFICATION_PREFERENCE_KEY) val silentNotificationsSystemOptions = findPreference(VectorPreferences.SETTINGS_SYSTEM_SILENT_NOTIFICATION_PREFERENCE_KEY)
if (supportNotificationChannels()) { if (NotificationUtils.supportNotificationChannels()) {
silentNotificationsSystemOptions.onPreferenceClickListener = Preference.OnPreferenceClickListener { silentNotificationsSystemOptions.onPreferenceClickListener = Preference.OnPreferenceClickListener {
NotificationUtils.openSystemSettingsForSilentCategory(this) NotificationUtils.openSystemSettingsForSilentCategory(this)
false false
@ -89,7 +88,7 @@ class VectorSettingsAdvancedNotificationPreferenceFragment : VectorSettingsBaseF
// Ringtone // Ringtone
val ringtonePreference = findPreference(VectorPreferences.SETTINGS_NOTIFICATION_RINGTONE_SELECTION_PREFERENCE_KEY) val ringtonePreference = findPreference(VectorPreferences.SETTINGS_NOTIFICATION_RINGTONE_SELECTION_PREFERENCE_KEY)
if (supportNotificationChannels()) { if (NotificationUtils.supportNotificationChannels()) {
ringtonePreference.isVisible = false ringtonePreference.isVisible = false
} else { } else {
ringtonePreference.summary = vectorPreferences.getNotificationRingToneName() ringtonePreference.summary = vectorPreferences.getNotificationRingToneName()