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