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:
- Add unread indent on room list (#485)
- Message Editing: Update notifications (#128)
- Remove any notification of a redacted event (#563)
Other changes:
-

View File

@ -43,6 +43,7 @@ interface PushRuleService {
interface PushRuleListener {
fun onMatchRule(event: Event, actions: List<Action>)
fun onRoomLeft(roomId: String)
fun onEventRedacted(redactedEventId: String)
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() {
try {
listeners.forEach {

View File

@ -78,6 +78,25 @@ internal class DefaultProcessEventForPushTask @Inject constructor(
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()
}

View File

@ -196,7 +196,6 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
// This ID can and should be used to detect duplicate notification requests.
val eventId = data["event_id"] ?: return //Just ignore
val eventType = data["type"]
if (eventType == null) {
//Just add a generic unknown event
@ -214,10 +213,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
)
notificationDrawerManager.onNotifiableEventReceived(simpleNotifiableEvent)
notificationDrawerManager.refreshNotificationDrawer()
return
} else {
val event = parseEvent(data) ?: return
val notifiableEvent = notifiableEventResolver.resolveEvent(event, session)
@ -228,8 +224,6 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
Timber.e("--> ${event}")
}
} else {
if (notifiableEvent is NotifiableMessageEvent) {
if (TextUtils.isEmpty(notifiableEvent.senderName)) {
notifiableEvent.senderName = data["sender_display_name"]
@ -246,7 +240,6 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
notificationDrawerManager.refreshNotificationDrawer()
}
}
}
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.NotificationUtils
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.settings.VectorPreferences
import im.vector.riotx.features.version.VersionProvider
@ -73,6 +72,7 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.
@Inject lateinit var pushRuleTriggerListener: PushRuleTriggerListener
@Inject lateinit var vectorPreferences: VectorPreferences
@Inject lateinit var versionProvider: VersionProvider
@Inject lateinit var notificationUtils: NotificationUtils
lateinit var vectorComponent: VectorComponent
private var fontThreadHandler: Handler? = null
@ -112,7 +112,7 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.
emojiCompatWrapper.init(fontRequest)
NotificationUtils.createNotificationChannels(applicationContext)
notificationUtils.createNotificationChannels()
if (authenticator.hasAuthenticatedSessions() && !activeSessionHolder.hasActiveSession()) {
val lastAuthenticatedSession = authenticator.getLastAuthenticatedSession()!!
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.html.EventHtmlRenderer
import im.vector.riotx.features.navigation.Navigator
import im.vector.riotx.features.notifications.NotifiableEventResolver
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.notifications.*
import im.vector.riotx.features.rageshake.BugReporter
import im.vector.riotx.features.rageshake.VectorFileLogger
import im.vector.riotx.features.rageshake.VectorUncaughtExceptionHandler
@ -58,6 +55,8 @@ interface VectorComponent {
fun currentSession(): Session
fun notificationUtils(): NotificationUtils
fun notificationDrawerManager(): NotificationDrawerManager
fun appContext(): Context
@ -72,7 +71,7 @@ interface VectorComponent {
fun emojiCompatFontProvider(): EmojiCompatFontProvider
fun emojiCompatWrapper() : EmojiCompatWrapper
fun emojiCompatWrapper(): EmojiCompatWrapper
fun eventHtmlRenderer(): EventHtmlRenderer

View File

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

View File

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

View File

@ -32,6 +32,7 @@ data class InviteNotifiableEvent(
override var isPushGatewayEvent: Boolean = false) : NotifiableEvent {
override var hasBeenDisplayed: Boolean = false
override var isRedacted: Boolean = false
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
var soundName: String?
var hasBeenDisplayed: Boolean
var isRedacted: Boolean
//Used to know if event should be replaced with the one coming from eventstream
var isPushGatewayEvent: Boolean
}

View File

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

View File

@ -15,7 +15,6 @@
*/
package im.vector.riotx.features.notifications
import android.app.Notification
import android.content.Context
import android.graphics.Bitmap
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.R
import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.features.settings.VectorPreferences
import me.gujun.android.span.span
import timber.log.Timber
@ -43,7 +43,9 @@ import javax.inject.Singleton
*/
@Singleton
class NotificationDrawerManager @Inject constructor(private val context: Context,
private val notificationUtils: NotificationUtils,
private val vectorPreferences: VectorPreferences,
private val stringProvider: StringProvider,
private val activeSessionHolder: ActiveSessionHolder,
private val iconLoader: IconLoader,
private val bitmapLoader: BitmapLoader,
@ -96,7 +98,6 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
notifiableEvent.noisy = false
eventList.remove(existing)
eventList.add(notifiableEvent)
} else {
//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
*/
@ -147,7 +157,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
e is NotifiableMessageEvent && e.roomId == roomId
}
}
NotificationUtils.cancelNotificationMessage(context, roomId, ROOM_MESSAGES_NOTIFICATION_ID)
notificationUtils.cancelNotificationMessage(roomId, ROOM_MESSAGES_NOTIFICATION_ID)
}
refreshNotificationDrawer()
}
@ -215,20 +225,15 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
val summaryInboxStyle = NotificationCompat.InboxStyle()
//group events by room to create a single MessagingStyle notif
val roomIdToEventMap: MutableMap<String, ArrayList<NotifiableMessageEvent>> = HashMap()
val simpleEvents: ArrayList<NotifiableEvent> = ArrayList()
val notifications: ArrayList<Notification> = ArrayList()
val roomIdToEventMap: MutableMap<String, MutableList<NotifiableMessageEvent>> = LinkedHashMap()
val simpleEvents: MutableList<NotifiableEvent> = ArrayList()
val eventIterator = eventList.listIterator()
while (eventIterator.hasNext()) {
val event = eventIterator.next()
if (event is NotifiableMessageEvent) {
val roomId = event.roomId
var roomEvents = roomIdToEventMap[roomId]
if (roomEvents == null) {
roomEvents = ArrayList()
roomIdToEventMap[roomId] = roomEvents
}
val roomEvents = roomIdToEventMap.getOrPut(roomId) { ArrayList() }
if (shouldIgnoreMessageEventInRoom(roomId) || outdatedDetector?.isMessageOutdated(event) == true) {
//forget this event
@ -246,13 +251,13 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
var globalLastMessageTimestamp = 0L
//events have been grouped
//events have been grouped by roomId
for ((roomId, events) in roomIdToEventMap) {
if (events.isEmpty()) {
// Build the notification for the room
if (events.isEmpty() || events.all { it.isRedacted }) {
//Just clear this notification
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
}
@ -280,7 +285,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
for (event in events) {
//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.customSound = event.soundName
}
@ -293,16 +298,18 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
.build()
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
} else {
if (!event.isRedacted) {
style.addMessage(event.body, event.timestamp, senderPerson)
}
}
event.hasBeenDisplayed = true //we can consider it as displayed
//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
NotificationUtils.cancelNotificationMessage(context, event.eventId, ROOM_EVENT_NOTIFICATION_ID)
notificationUtils.cancelNotificationMessage(event.eventId, ROOM_EVENT_NOTIFICATION_ID)
}
try {
@ -328,7 +335,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
summaryInboxStyle.addLine(line)
}
} else {
val summaryLine = context.resources.getQuantityString(
val summaryLine = stringProvider.getQuantityString(
R.plurals.notification_compat_summary_line_for_room, events.size, roomName, events.size)
summaryInboxStyle.addLine(summaryLine)
}
@ -347,18 +354,16 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
globalLastMessageTimestamp = lastMessageTimestamp
}
NotificationUtils.buildMessagesListNotification(context,
vectorPreferences,
val notification = notificationUtils.buildMessagesListNotification(
style,
roomEventGroupInfo,
largeBitmap,
lastMessageTimestamp,
myUserDisplayName)
?.let {
//is there an id for this room?
notifications.add(it)
NotificationUtils.showNotificationMessage(context, roomId, ROOM_MESSAGES_NOTIFICATION_ID, it)
}
notificationUtils.showNotificationMessage(roomId, ROOM_MESSAGES_NOTIFICATION_ID, notification)
hasNewEvent = true
summaryIsNoisy = summaryIsNoisy || roomEventGroupInfo.shouldBing
} else {
@ -371,16 +376,14 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
for (event in simpleEvents) {
//We build a simple event
if (firstTime || !event.hasBeenDisplayed) {
NotificationUtils.buildSimpleEventNotification(context, vectorPreferences, event, null, session.myUserId)?.let {
notifications.add(it)
NotificationUtils.showNotificationMessage(context, event.eventId, ROOM_EVENT_NOTIFICATION_ID, it)
val notification = notificationUtils.buildSimpleEventNotification(event, null, session.myUserId)
notificationUtils.showNotificationMessage(event.eventId, ROOM_EVENT_NOTIFICATION_ID, notification)
event.hasBeenDisplayed = true //we can consider it as displayed
hasNewEvent = true
summaryIsNoisy = summaryIsNoisy || event.noisy
summaryInboxStyle.addLine(event.description)
}
}
}
//======== Build summary notification =========
@ -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
// https://developer.android.com/training/notify-user/group
if (eventList.isEmpty()) {
NotificationUtils.cancelNotificationMessage(context, null, SUMMARY_NOTIFICATION_ID)
if (eventList.isEmpty() || eventList.all { it.isRedacted }) {
notificationUtils.cancelNotificationMessage(null, SUMMARY_NOTIFICATION_ID)
} else {
val nbEvents = roomIdToEventMap.size + simpleEvents.size
val sumTitle = context.resources.getQuantityString(
R.plurals.notification_compat_summary_title, nbEvents, nbEvents)
val sumTitle = stringProvider.getQuantityString(R.plurals.notification_compat_summary_title, nbEvents, nbEvents)
summaryInboxStyle.setBigContentTitle(sumTitle)
//TODO get latest event?
.setSummaryText(
context.resources
.getQuantityString(R.plurals.notification_unread_notified_messages, nbEvents, nbEvents))
.setSummaryText(stringProvider.getQuantityString(R.plurals.notification_unread_notified_messages, nbEvents, nbEvents))
NotificationUtils.buildSummaryListNotification(
context,
vectorPreferences,
val notification = notificationUtils.buildSummaryListNotification(
summaryInboxStyle,
sumTitle,
noisy = hasNewEvent && summaryIsNoisy,
lastMessageTimestamp = globalLastMessageTimestamp
)?.let {
NotificationUtils.showNotificationMessage(context, null, SUMMARY_NOTIFICATION_ID, it)
}
lastMessageTimestamp = globalLastMessageTimestamp)
notificationUtils.showNotificationMessage(null, SUMMARY_NOTIFICATION_ID, notification)
if (hasNewEvent && summaryIsNoisy) {
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
//Use the last event (most recent?)
val roomAvatarPath = events.last().roomAvatarPath
?: events.last().senderAvatarPath
val roomAvatarPath = events.last().roomAvatarPath ?: events.last().senderAvatarPath
return bitmapLoader.getRoomBitmap(roomAvatarPath)
}
@ -461,7 +457,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
fun persistInfo() {
synchronized(eventList) {
if (eventList.isEmpty()) {
deleteCachedRoomNotifications(context)
deleteCachedRoomNotifications()
return
}
try {
@ -476,14 +472,14 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
}
}
private fun loadEventInfo(): ArrayList<NotifiableEvent> {
private fun loadEventInfo(): MutableList<NotifiableEvent> {
try {
val file = File(context.applicationContext.cacheDir, ROOMS_NOTIFICATIONS_FILE_NAME)
if (file.exists()) {
FileInputStream(file).use {
val events: ArrayList<NotifiableEvent>? = activeSessionHolder.getSafeActiveSession()?.loadSecureSecret(it, KEY_ALIAS_SECRET_STORAGE)
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()
}
private fun deleteCachedRoomNotifications(context: Context) {
private fun deleteCachedRoomNotifications() {
val file = File(context.applicationContext.cacheDir, ROOMS_NOTIFICATIONS_FILE_NAME)
if (file.exists()) {
file.delete()

View File

@ -27,7 +27,6 @@ import android.content.Intent
import android.graphics.Bitmap
import android.net.Uri
import android.os.Build
import android.text.TextUtils
import androidx.annotation.StringRes
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
@ -37,6 +36,7 @@ import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import im.vector.riotx.BuildConfig
import im.vector.riotx.R
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.core.utils.startNotificationChannelSettingsIntent
import im.vector.riotx.features.home.HomeActivity
import im.vector.riotx.features.home.room.detail.RoomDetailActivity
@ -44,15 +44,20 @@ import im.vector.riotx.features.home.room.detail.RoomDetailArgs
import im.vector.riotx.features.settings.VectorPreferences
import timber.log.Timber
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.
* 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
* ========================================================================================== */
@ -90,23 +95,36 @@ object NotificationUtils {
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"
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
* ========================================================================================== */
/**
* Create notification channels.
*
* @param context the context
*/
@TargetApi(Build.VERSION_CODES.O)
fun createNotificationChannels(context: Context) {
fun createNotificationChannels() {
if (!supportNotificationChannels()) {
return
}
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
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
@ -132,10 +150,10 @@ object NotificationUtils {
* intrude.
*/
notificationManager.createNotificationChannel(NotificationChannel(NOISY_NOTIFICATION_CHANNEL_ID,
context.getString(R.string.notification_noisy_notifications),
stringProvider.getString(R.string.notification_noisy_notifications),
NotificationManager.IMPORTANCE_DEFAULT)
.apply {
description = context.getString(R.string.notification_noisy_notifications)
description = stringProvider.getString(R.string.notification_noisy_notifications)
enableVibration(true)
enableLights(true)
lightColor = accentColor
@ -145,29 +163,29 @@ object NotificationUtils {
* Low notification importance: shows everywhere, but is not intrusive.
*/
notificationManager.createNotificationChannel(NotificationChannel(SILENT_NOTIFICATION_CHANNEL_ID,
context.getString(R.string.notification_silent_notifications),
stringProvider.getString(R.string.notification_silent_notifications),
NotificationManager.IMPORTANCE_LOW)
.apply {
description = context.getString(R.string.notification_silent_notifications)
description = stringProvider.getString(R.string.notification_silent_notifications)
setSound(null, null)
enableLights(true)
lightColor = accentColor
})
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)
.apply {
description = context.getString(R.string.notification_listening_for_events)
description = stringProvider.getString(R.string.notification_listening_for_events)
setSound(null, null)
setShowBadge(false)
})
notificationManager.createNotificationChannel(NotificationChannel(CALL_NOTIFICATION_CHANNEL_ID,
context.getString(R.string.call),
stringProvider.getString(R.string.call),
NotificationManager.IMPORTANCE_HIGH)
.apply {
description = context.getString(R.string.call)
description = stringProvider.getString(R.string.call)
setSound(null, null)
enableLights(true)
lightColor = accentColor
@ -177,12 +195,11 @@ object NotificationUtils {
/**
* Build a polling thread listener notification
*
* @param context Android context
* @param subTitleResId subtitle string resource Id of the notification
* @return the polling thread listener notification
*/
@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.
val i = Intent(context, HomeActivity::class.java)
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 builder = NotificationCompat.Builder(context, LISTENING_FOR_EVENTS_NOTIFICATION_CHANNEL_ID)
.setContentTitle(context.getString(subTitleResId))
.setContentTitle(stringProvider.getString(subTitleResId))
.setSmallIcon(R.drawable.sync)
.setCategory(NotificationCompat.CATEGORY_SERVICE)
.setColor(accentColor)
@ -225,7 +242,7 @@ object NotificationUtils {
CharSequence::class.java,
CharSequence::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) {
Timber.e(ex, "## buildNotification(): Exception - setLatestEventInfo() Msg=")
}
@ -238,7 +255,6 @@ object NotificationUtils {
* Build an incoming call notification.
* 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 roomName the room name in which the call is pending.
* @param matrixId the matrix id
@ -246,20 +262,19 @@ object NotificationUtils {
* @return the call notification.
*/
@SuppressLint("NewApi")
fun buildIncomingCallNotification(context: Context,
isVideo: Boolean,
fun buildIncomingCallNotification(isVideo: Boolean,
roomName: String,
matrixId: String,
callId: String): Notification {
val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color)
val builder = NotificationCompat.Builder(context, CALL_NOTIFICATION_CHANNEL_ID)
.setContentTitle(ensureTitleNotEmpty(context, roomName))
.setContentTitle(ensureTitleNotEmpty(roomName))
.apply {
if (isVideo) {
setContentText(context.getString(R.string.incoming_video_call))
setContentText(stringProvider.getString(R.string.incoming_video_call))
} else {
setContentText(context.getString(R.string.incoming_voice_call))
setContentText(stringProvider.getString(R.string.incoming_voice_call))
}
}
.setSmallIcon(R.drawable.incoming_call_notification_transparent)
@ -295,7 +310,6 @@ object NotificationUtils {
/**
* Build a pending call notification
*
* @param context the context.
* @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 roomId the room Id
@ -304,20 +318,19 @@ object NotificationUtils {
* @return the call notification.
*/
@SuppressLint("NewApi")
fun buildPendingCallNotification(context: Context,
isVideo: Boolean,
fun buildPendingCallNotification(isVideo: Boolean,
roomName: String,
roomId: String,
matrixId: String,
callId: String): Notification {
val builder = NotificationCompat.Builder(context, CALL_NOTIFICATION_CHANNEL_ID)
.setContentTitle(ensureTitleNotEmpty(context, roomName))
.setContentTitle(ensureTitleNotEmpty(roomName))
.apply {
if (isVideo) {
setContentText(context.getString(R.string.video_call_in_progress))
setContentText(stringProvider.getString(R.string.video_call_in_progress))
} else {
setContentText(context.getString(R.string.call_in_progress))
setContentText(stringProvider.getString(R.string.call_in_progress))
}
}
.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
*/
fun buildCallEndedNotification(context: Context): Notification {
fun buildCallEndedNotification(): Notification {
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)
.setCategory(NotificationCompat.CATEGORY_CALL)
.build()
@ -366,17 +379,14 @@ object NotificationUtils {
/**
* Build a notification for a Room
*/
fun buildMessagesListNotification(context: Context,
vectorPreferences: VectorPreferences,
messageStyle: NotificationCompat.MessagingStyle,
fun buildMessagesListNotification(messageStyle: NotificationCompat.MessagingStyle,
roomInfo: RoomEventGroupInfo,
largeIcon: Bitmap?,
lastMessageTimestamp: Long,
senderDisplayNameForReplyCompat: String?): Notification? {
senderDisplayNameForReplyCompat: String?): Notification {
val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color)
// 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 channelID = if (roomInfo.shouldBing) NOISY_NOTIFICATION_CHANNEL_ID else SILENT_NOTIFICATION_CHANNEL_ID
@ -393,18 +403,15 @@ object NotificationUtils {
// Title for API < 16 devices.
.setContentTitle(roomInfo.roomDisplayName)
// 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.
.setSubText(context
.resources
.getQuantityString(R.plurals.room_new_messages_notification, messageStyle.messages.size, messageStyle.messages.size)
)
.setSubText(stringProvider.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+)
// devices and all Wear devices. But we want a custom grouping, so we specify the groupID
// 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)
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY)
@ -440,17 +447,17 @@ object NotificationUtils {
addAction(NotificationCompat.Action(
R.drawable.ic_material_done_all_white,
context.getString(R.string.action_mark_room_read),
stringProvider.getString(R.string.action_mark_room_read),
markRoomReadPendingIntent))
// Quick reply
if (!roomInfo.hasSmartReplyError) {
buildQuickReplyIntent(context, roomInfo.roomId, senderDisplayNameForReplyCompat)?.let { replyPendingIntent ->
buildQuickReplyIntent(roomInfo.roomId, senderDisplayNameForReplyCompat)?.let { replyPendingIntent ->
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()
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)
.build()?.let {
addAction(it)
@ -477,11 +484,9 @@ object NotificationUtils {
}
fun buildSimpleEventNotification(context: Context,
vectorPreferences: VectorPreferences,
simpleNotifiableEvent: NotifiableEvent,
fun buildSimpleEventNotification(simpleNotifiableEvent: NotifiableEvent,
largeIcon: Bitmap?,
matrixId: String): Notification? {
matrixId: String): Notification {
val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color)
// Build the pending intent for when the notification is clicked
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
return NotificationCompat.Builder(context, channelID)
.setContentTitle(context.getString(R.string.app_name))
.setContentTitle(stringProvider.getString(R.string.app_name))
.setContentText(simpleNotifiableEvent.description)
.setGroup(context.getString(R.string.app_name))
.setGroup(stringProvider.getString(R.string.app_name))
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY)
.setSmallIcon(smallIcon)
.setColor(accentColor)
@ -508,7 +513,7 @@ object NotificationUtils {
addAction(
R.drawable.vector_notification_reject_invitation,
context.getString(R.string.reject),
stringProvider.getString(R.string.reject),
rejectIntentPendingIntent)
// offer to type a quick accept button
@ -520,7 +525,7 @@ object NotificationUtils {
PendingIntent.FLAG_UPDATE_CURRENT)
addAction(
R.drawable.vector_notification_accept_invitation,
context.getString(R.string.join),
stringProvider.getString(R.string.join),
joinIntentPendingIntent)
} else {
setAutoCancel(true)
@ -551,7 +556,7 @@ object NotificationUtils {
.build()
}
private fun buildOpenRoomIntent(context: Context, roomId: String): PendingIntent? {
private fun buildOpenRoomIntent(roomId: String): PendingIntent? {
val roomIntentTap = RoomDetailActivity.newIntent(context, RoomDetailArgs(roomId))
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
@ -564,7 +569,7 @@ object NotificationUtils {
.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)
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
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),
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
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
intent = Intent(context, NotificationBroadcastReceiver::class.java)
@ -610,12 +615,10 @@ object NotificationUtils {
/**
* Build the summary notification
*/
fun buildSummaryListNotification(context: Context,
vectorPreferences: VectorPreferences,
style: NotificationCompat.InboxStyle,
fun buildSummaryListNotification(style: NotificationCompat.InboxStyle,
compatSummary: String,
noisy: Boolean,
lastMessageTimestamp: Long): Notification? {
lastMessageTimestamp: Long): Notification {
val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color)
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
.setWhen(lastMessageTimestamp)
.setStyle(style)
.setContentTitle(context.getString(R.string.app_name))
.setContentTitle(stringProvider.getString(R.string.app_name))
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
.setSmallIcon(smallIcon)
//set content text to support devices running API level < 24
.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
.setGroupSummary(true)
.setColor(accentColor)
@ -645,13 +648,12 @@ object NotificationUtils {
priority = NotificationCompat.PRIORITY_LOW
}
}
.setContentIntent(buildOpenHomePendingIntentForSummary(context))
.setDeleteIntent(getDismissSummaryPendingIntent(context))
.setContentIntent(buildOpenHomePendingIntentForSummary())
.setDeleteIntent(getDismissSummaryPendingIntent())
.build()
}
private fun getDismissSummaryPendingIntent(context: Context): PendingIntent {
private fun getDismissSummaryPendingIntent(): PendingIntent {
val intent = Intent(context, NotificationBroadcastReceiver::class.java)
intent.action = DISMISS_SUMMARY_ACTION
intent.data = Uri.parse("foobar://deleteSummary")
@ -659,33 +661,28 @@ object NotificationUtils {
0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
}
fun showNotificationMessage(context: Context, tag: String?, id: Int, notification: Notification) {
with(NotificationManagerCompat.from(context)) {
notify(tag, id, notification)
}
fun showNotificationMessage(tag: String?, id: Int, notification: Notification) {
notificationManager.notify(tag, id, notification)
}
fun cancelNotificationMessage(context: Context, tag: String?, id: Int) {
NotificationManagerCompat.from(context)
.cancel(tag, id)
fun cancelNotificationMessage(tag: String?, id: Int) {
notificationManager.cancel(tag, id)
}
/**
* Cancel the foreground notification service
*/
fun cancelNotificationForegroundService(context: Context) {
NotificationManagerCompat.from(context)
.cancel(NOTIFICATION_ID_FOREGROUND_SERVICE)
fun cancelNotificationForegroundService() {
notificationManager.cancel(NOTIFICATION_ID_FOREGROUND_SERVICE)
}
/**
* Cancel all the notification
*/
fun cancelAllNotifications(context: Context) {
fun cancelAllNotifications() {
// Keep this try catch (reported by GA)
try {
NotificationManagerCompat.from(context)
.cancelAll()
notificationManager.cancelAll()
} catch (e: Exception) {
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
*/
fun isDoNotDisturbModeOn(context: Context): Boolean {
fun isDoNotDisturbModeOn(): Boolean {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
return false
}
@ -706,23 +703,11 @@ object NotificationUtils {
|| setting == NotificationManager.INTERRUPTION_FILTER_ALARMS
}
private fun ensureTitleNotEmpty(context: Context, title: String?): CharSequence {
if (TextUtils.isEmpty(title)) {
return context.getString(R.string.app_name)
private fun ensureTitleNotEmpty(title: String?): CharSequence {
if (title.isNullOrBlank()) {
return stringProvider.getString(R.string.app_name)
}
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)
return title
}
}

View File

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

View File

@ -30,6 +30,7 @@ data class SimpleNotifiableEvent(
override var isPushGatewayEvent: Boolean = false) : NotifiableEvent {
override var hasBeenDisplayed: Boolean = false
override var isRedacted: Boolean = false
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.BingRulePreference
import im.vector.riotx.features.notifications.NotificationUtils
import im.vector.riotx.features.notifications.supportNotificationChannels
import javax.inject.Inject
class VectorSettingsAdvancedNotificationPreferenceFragment : VectorSettingsBaseFragment() {
@ -56,7 +55,7 @@ class VectorSettingsAdvancedNotificationPreferenceFragment : VectorSettingsBaseF
override fun bindPref() {
val callNotificationsSystemOptions = findPreference(VectorPreferences.SETTINGS_SYSTEM_CALL_NOTIFICATION_PREFERENCE_KEY)
if (supportNotificationChannels()) {
if (NotificationUtils.supportNotificationChannels()) {
callNotificationsSystemOptions.onPreferenceClickListener = Preference.OnPreferenceClickListener {
NotificationUtils.openSystemSettingsForCallCategory(this)
false
@ -66,7 +65,7 @@ class VectorSettingsAdvancedNotificationPreferenceFragment : VectorSettingsBaseF
}
val noisyNotificationsSystemOptions = findPreference(VectorPreferences.SETTINGS_SYSTEM_NOISY_NOTIFICATION_PREFERENCE_KEY)
if (supportNotificationChannels()) {
if (NotificationUtils.supportNotificationChannels()) {
noisyNotificationsSystemOptions.onPreferenceClickListener = Preference.OnPreferenceClickListener {
NotificationUtils.openSystemSettingsForNoisyCategory(this)
false
@ -76,7 +75,7 @@ class VectorSettingsAdvancedNotificationPreferenceFragment : VectorSettingsBaseF
}
val silentNotificationsSystemOptions = findPreference(VectorPreferences.SETTINGS_SYSTEM_SILENT_NOTIFICATION_PREFERENCE_KEY)
if (supportNotificationChannels()) {
if (NotificationUtils.supportNotificationChannels()) {
silentNotificationsSystemOptions.onPreferenceClickListener = Preference.OnPreferenceClickListener {
NotificationUtils.openSystemSettingsForSilentCategory(this)
false
@ -89,7 +88,7 @@ class VectorSettingsAdvancedNotificationPreferenceFragment : VectorSettingsBaseF
// Ringtone
val ringtonePreference = findPreference(VectorPreferences.SETTINGS_NOTIFICATION_RINGTONE_SELECTION_PREFERENCE_KEY)
if (supportNotificationChannels()) {
if (NotificationUtils.supportNotificationChannels()) {
ringtonePreference.isVisible = false
} else {
ringtonePreference.summary = vectorPreferences.getNotificationRingToneName()