Add support for lower APIs to new notification system.

fork-5.53.8
Cody Henthorne 2021-04-28 15:22:13 -04:00 zatwierdzone przez Alex Hart
rodzic ab44d608d2
commit bd2a1d5574
10 zmienionych plików z 238 dodań i 112 usunięć

Wyświetl plik

@ -80,6 +80,10 @@ android {
flavorDimensions 'distribution', 'environment'
useLibrary 'org.apache.http.legacy'
kotlinOptions {
freeCompilerArgs = ["-Xallow-result-return-type"]
}
dexOptions {
javaMaxHeapSize "4g"
}

Wyświetl plik

@ -79,7 +79,7 @@ public final class NotificationCancellationHelper {
}
public static void cancelMessageSummaryIfSoleNotification(@NonNull Context context) {
if (Build.VERSION.SDK_INT >= 23) {
if (Build.VERSION.SDK_INT > 23) {
try {
NotificationManager notifications = ServiceUtil.getNotificationManager(context);
StatusBarNotification[] activeNotifications = notifications.getActiveNotifications();

Wyświetl plik

@ -14,6 +14,7 @@ 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.database.MessageDatabase
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.messages.IncomingMessageObserver
import org.thoughtcrime.securesms.notifications.DefaultMessageNotifier
@ -29,6 +30,7 @@ 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.lang.UnsupportedOperationException
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.Executor
import java.util.concurrent.Executors
@ -125,9 +127,26 @@ class MessageNotifierV2(context: Application) : MessageNotifier {
}
Log.internal().i(TAG, "sticky thread: $stickyThreads")
val state: NotificationStateV2 = NotificationStateProvider.constructNotificationState(context, stickyThreads)
var state: NotificationStateV2 = NotificationStateProvider.constructNotificationState(context, stickyThreads)
Log.internal().i(TAG, "state: $state")
val displayedNotifications: Set<Int>? = ServiceUtil.getNotificationManager(context).getDisplayedNotificationIds().getOrNull()
if (displayedNotifications != null) {
val cleanedUpThreadIds: MutableSet<Long> = mutableSetOf()
state.conversations.filterNot { it.hasNewNotifications() || displayedNotifications.contains(it.notificationId) }
.forEach { conversation ->
cleanedUpThreadIds += conversation.threadId
conversation.notificationItems.forEach { item ->
val messageDatabase: MessageDatabase = if (item.isMms) DatabaseFactory.getMmsDatabase(context) else DatabaseFactory.getSmsDatabase(context)
messageDatabase.markAsNotified(item.id)
}
}
if (cleanedUpThreadIds.isNotEmpty()) {
Log.i(TAG, "Cleaned up ${cleanedUpThreadIds.size} thread(s) with dangling notifications")
state = NotificationStateV2(state.conversations.filterNot { cleanedUpThreadIds.contains(it.threadId) })
}
}
val retainStickyThreadIds: Set<Long> = state.getThreadsWithMostRecentNotificationFromSelf()
stickyThreads.keys.retainAll { retainStickyThreadIds.contains(it) }
@ -148,7 +167,6 @@ class MessageNotifierV2(context: Application) : MessageNotifier {
targetThreadId = threadId,
defaultBubbleState = defaultBubbleState,
lastAudibleNotification = lastAudibleNotification,
notificationConfigurationChanged = notificationConfigurationChanged,
alertOverrides = alertOverrides
)
@ -238,8 +256,8 @@ class MessageNotifierV2(context: Application) : MessageNotifier {
}
companion object {
private val TAG = Log.tag(MessageNotifierV2::class.java)
private val REMINDER_TIMEOUT = TimeUnit.MINUTES.toMillis(2)
val TAG: String = Log.tag(MessageNotifierV2::class.java)
private val REMINDER_TIMEOUT: Long = TimeUnit.MINUTES.toMillis(2)
private fun updateBadge(context: Context, count: Int) {
try {
@ -250,36 +268,51 @@ class MessageNotifierV2(context: Application) : MessageNotifier {
}
}
private fun NotificationManager.cancelOrphanedNotifications(context: Context, state: NotificationStateV2, stickyNotifications: Set<Int>) {
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) &&
!stickyNotifications.contains(notification.id)
) {
if (!state.notificationIds.contains(notification.id)) {
Log.d(TAG, "Cancelling orphaned notification: ${notification.id}")
NotificationCancellationHelper.cancel(context, notification.id)
}
}
}
NotificationCancellationHelper.cancelMessageSummaryIfSoleNotification(context)
} catch (e: Throwable) {
Log.w(TAG, e)
}
}
data class StickyThread(val threadId: Long, val notificationId: Int, val earliestTimestamp: Long)
private data class Reminder(val lastNotified: Long, val count: Int = 0)
}
private fun StatusBarNotification.isMessageNotification(): Boolean {
return id != NotificationIds.MESSAGE_SUMMARY &&
id != KeyCachingService.SERVICE_RUNNING_ID &&
id != IncomingMessageObserver.FOREGROUND_ID &&
id != NotificationIds.PENDING_MESSAGES &&
!CallNotificationBuilder.isWebRtcNotification(id)
}
private fun NotificationManager.getDisplayedNotificationIds(): Result<Set<Int>> {
if (Build.VERSION.SDK_INT < 23) {
return Result.failure(UnsupportedOperationException("SDK level too low"))
}
return try {
Result.success(activeNotifications.filter { it.isMessageNotification() }.map { it.id }.toSet())
} catch (e: Throwable) {
Log.w(MessageNotifierV2.TAG, e)
Result.failure(e)
}
}
private fun NotificationManager.cancelOrphanedNotifications(context: Context, state: NotificationStateV2, stickyNotifications: Set<Int>) {
if (Build.VERSION.SDK_INT < 23) {
return
}
try {
activeNotifications.filter { it.isMessageNotification() && !stickyNotifications.contains(it.id) }
.map { it.id }
.filterNot { state.notificationIds.contains(it) }
.forEach { id ->
Log.d(MessageNotifierV2.TAG, "Cancelling orphaned notification: $id")
NotificationCancellationHelper.cancel(context, id)
}
NotificationCancellationHelper.cancelMessageSummaryIfSoleNotification(context)
} catch (e: Throwable) {
Log.w(MessageNotifierV2.TAG, e)
}
}
private class CancelableExecutor {
private val executor: Executor = Executors.newSingleThreadExecutor()
private val tasks: MutableSet<DelayedNotification> = mutableSetOf()

Wyświetl plik

@ -332,10 +332,18 @@ sealed class NotificationBuilder(protected val context: Context) {
}
override fun setGroup(group: String) {
if (Build.VERSION.SDK_INT < 23) {
return
}
builder.setGroup(group)
}
override fun setGroupAlertBehavior(behavior: Int) {
if (Build.VERSION.SDK_INT < 23) {
return
}
builder.setGroupAlertBehavior(behavior)
}
@ -479,15 +487,6 @@ sealed class NotificationBuilder(protected val context: Context) {
}
override fun addMessagesActual(conversation: NotificationConversation, includeShortcut: Boolean) {
val bigPictureUri: Uri? = conversation.getSlideBigPictureUri(context)
if (bigPictureUri != null) {
builder.style = Notification.BigPictureStyle()
.bigPicture(bigPictureUri.toBitmap(context, BIG_PICTURE_DIMEN))
.setSummaryText(conversation.getContentText(context))
.bigLargeIcon(null as Bitmap?)
return
}
val self: Person = Person.Builder()
.setBot(false)
.setName(Recipient.self().getDisplayName(context))

Wyświetl plik

@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.notifications.v2
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.net.Uri
import android.text.SpannableStringBuilder
@ -25,13 +24,11 @@ import org.thoughtcrime.securesms.service.KeyCachingService
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.thoughtcrime.securesms.util.Util
private const val LARGE_ICON_DIMEN = 250
/**
* Encapsulate all the notifications for a given conversation (thread) and the top
* level information about said conversation.
*/
class NotificationConversation(
data class NotificationConversation(
val recipient: Recipient,
val threadId: Long,
val notificationItems: List<NotificationItemV2>
@ -51,18 +48,7 @@ class NotificationConversation(
}
}
fun getLargeIcon(context: Context): Bitmap? {
if (TextSecurePreferences.getNotificationPrivacy(context).isDisplayMessage) {
val largeIconUri: Uri? = getSlideLargeIcon()
if (largeIconUri != null) {
return largeIconUri.toBitmap(context, LARGE_ICON_DIMEN)
}
}
return getContactLargeIcon(context).toLargeBitmap(context)
}
private fun getContactLargeIcon(context: Context): Drawable? {
fun getContactLargeIcon(context: Context): Drawable? {
return if (TextSecurePreferences.getNotificationPrivacy(context).isDisplayContact) {
recipient.getContactDrawable(context)
} else {
@ -78,10 +64,6 @@ class NotificationConversation(
}
}
private fun getSlideLargeIcon(): Uri? {
return if (notificationItems.size == 1) mostRecentNotification.getLargeIconUri() else null
}
fun getSlideBigPictureUri(context: Context): Uri? {
return if (notificationItems.size == 1 && TextSecurePreferences.getNotificationPrivacy(context).isDisplayMessage && !KeyCachingService.isLocked(context)) {
mostRecentNotification.getBigPictureUri()

Wyświetl plik

@ -1,5 +1,6 @@
package org.thoughtcrime.securesms.notifications.v2
import android.annotation.TargetApi
import android.app.Notification
import android.app.NotificationManager
import android.app.PendingIntent
@ -44,67 +45,114 @@ object NotificationFactory {
targetThreadId: Long,
defaultBubbleState: BubbleUtil.BubbleState,
lastAudibleNotification: Long,
notificationConfigurationChanged: Boolean,
alertOverrides: Set<Long>
): Set<Long> {
if (state.isEmpty) {
Log.d(TAG, "State is empty, bailing")
return emptySet()
}
val nonVisibleThreadCount = state.conversations.count { it.threadId != visibleThreadId }
return if (Build.VERSION.SDK_INT < 23) {
notify19(
context = context,
state = state,
visibleThreadId = visibleThreadId,
targetThreadId = targetThreadId,
defaultBubbleState = defaultBubbleState,
lastAudibleNotification = lastAudibleNotification,
alertOverrides = alertOverrides,
nonVisibleThreadCount = nonVisibleThreadCount
)
} else {
notify23(
context = context,
state = state,
visibleThreadId = visibleThreadId,
targetThreadId = targetThreadId,
defaultBubbleState = defaultBubbleState,
lastAudibleNotification = lastAudibleNotification,
alertOverrides = alertOverrides,
nonVisibleThreadCount = nonVisibleThreadCount
)
}
}
private fun notify19(
context: Context,
state: NotificationStateV2,
visibleThreadId: Long,
targetThreadId: Long,
defaultBubbleState: BubbleUtil.BubbleState,
lastAudibleNotification: Long,
alertOverrides: Set<Long>,
nonVisibleThreadCount: Int
): Set<Long> {
val threadsThatNewlyAlerted: MutableSet<Long> = mutableSetOf()
if (Build.VERSION.SDK_INT >= 23 || state.conversations.size == 1) {
state.conversations.forEach { conversation ->
if (conversation.threadId == visibleThreadId && conversation.hasNewNotifications()) {
Log.internal().i(TAG, "Thread is visible, notifying in thread. notificationId: ${conversation.notificationId}")
notifyInThread(context, conversation.recipient, lastAudibleNotification)
} else if (notificationConfigurationChanged || conversation.hasNewNotifications() || alertOverrides.contains(conversation.threadId)) {
if (conversation.hasNewNotifications()) {
threadsThatNewlyAlerted += conversation.threadId
}
notifyForConversation(
context = context,
conversation = conversation,
targetThreadId = targetThreadId,
defaultBubbleState = defaultBubbleState,
shouldAlert = (conversation.hasNewNotifications() || alertOverrides.contains(conversation.threadId)) && !conversation.mostRecentNotification.individualRecipient.isSelf
)
}
state.conversations.find { it.threadId == visibleThreadId }?.let { conversation ->
if (conversation.hasNewNotifications()) {
Log.internal().i(TAG, "Thread is visible, notifying in thread. notificationId: ${conversation.notificationId}")
notifyInThread(context, conversation.recipient, lastAudibleNotification)
}
}
if ((state.conversations.size > 1 && threadsThatNewlyAlerted.isNotEmpty()) || ServiceUtil.getNotificationManager(context).isDisplayingSummaryNotification()) {
val builder: NotificationBuilder = NotificationBuilder.create(context)
builder.apply {
setSmallIcon(R.drawable.ic_notification)
setColor(ContextCompat.getColor(context, R.color.core_ultramarine))
setCategory(NotificationCompat.CATEGORY_MESSAGE)
setGroup(DefaultMessageNotifier.NOTIFICATION_GROUP)
setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN)
setChannelId(NotificationChannels.getMessagesChannel(context))
setContentTitle(context.getString(R.string.app_name))
setContentIntent(PendingIntent.getActivity(context, 0, MainActivity.clearTop(context), 0))
setGroupSummary(true)
setSubText(context.getString(R.string.MessageNotifier_d_new_messages_in_d_conversations, state.messageCount, state.threadCount))
setContentInfo(state.messageCount.toString())
setNumber(state.messageCount)
setSummaryContentText(state.mostRecentSender)
setDeleteIntent(state.getDeleteIntent(context))
setWhen(state.mostRecentNotification)
addMarkAsReadAction(state)
addMessages(state)
setOnlyAlertOnce(!state.notificationItems.any { it.isNewNotification })
setPriority(TextSecurePreferences.getNotificationPriority(context))
setLights()
setAlarms(state.mostRecentSender)
setTicker(state.mostRecentNotification.getStyledPrimaryText(context, true))
if (nonVisibleThreadCount == 1) {
state.conversations.first { it.threadId != visibleThreadId }.let { conversation ->
notifyForConversation(
context = context,
conversation = conversation,
targetThreadId = targetThreadId,
defaultBubbleState = defaultBubbleState,
shouldAlert = (conversation.hasNewNotifications() || alertOverrides.contains(conversation.threadId)) && !conversation.mostRecentNotification.individualRecipient.isSelf
)
if (conversation.hasNewNotifications()) {
threadsThatNewlyAlerted += conversation.threadId
}
}
} else if (nonVisibleThreadCount > 1) {
val nonVisibleConversations: List<NotificationConversation> = state.getNonVisibleConversation(visibleThreadId)
threadsThatNewlyAlerted += nonVisibleConversations.filter { it.hasNewNotifications() }.map { it.threadId }
notifySummary(context = context, state = state.copy(conversations = nonVisibleConversations))
}
Log.d(TAG, "showing summary notification")
NotificationManagerCompat.from(context).safelyNotify(context, null, NotificationIds.MESSAGE_SUMMARY, builder.build())
return threadsThatNewlyAlerted
}
@TargetApi(23)
private fun notify23(
context: Context,
state: NotificationStateV2,
visibleThreadId: Long,
targetThreadId: Long,
defaultBubbleState: BubbleUtil.BubbleState,
lastAudibleNotification: Long,
alertOverrides: Set<Long>,
nonVisibleThreadCount: Int
): Set<Long> {
val threadsThatNewlyAlerted: MutableSet<Long> = mutableSetOf()
state.conversations.forEach { conversation ->
if (conversation.threadId == visibleThreadId && conversation.hasNewNotifications()) {
Log.internal().i(TAG, "Thread is visible, notifying in thread. notificationId: ${conversation.notificationId}")
notifyInThread(context, conversation.recipient, lastAudibleNotification)
} else {
if (conversation.hasNewNotifications()) {
threadsThatNewlyAlerted += conversation.threadId
}
notifyForConversation(
context = context,
conversation = conversation,
targetThreadId = targetThreadId,
defaultBubbleState = defaultBubbleState,
shouldAlert = (conversation.hasNewNotifications() || alertOverrides.contains(conversation.threadId)) && !conversation.mostRecentNotification.individualRecipient.isSelf
)
}
}
if (nonVisibleThreadCount > 1 || ServiceUtil.getNotificationManager(context).isDisplayingSummaryNotification()) {
notifySummary(context = context, state = state.copy(conversations = state.getNonVisibleConversation(visibleThreadId)))
}
return threadsThatNewlyAlerted
@ -127,7 +175,7 @@ object NotificationFactory {
setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN)
setChannelId(conversation.getChannelId(context))
setContentTitle(conversation.getContentTitle(context))
setLargeIcon(conversation.getLargeIcon(context))
setLargeIcon(conversation.getContactLargeIcon(context).toLargeBitmap(context))
addPerson(conversation.recipient)
setShortcutId(ConversationUtil.getShortcutId(conversation.recipient))
setContentInfo(conversation.messageCount.toString())
@ -151,7 +199,41 @@ object NotificationFactory {
builder.addTurnOffJoinedNotificationsAction(conversation.getTurnOffJoinedNotificationsIntent(context))
}
NotificationManagerCompat.from(context).safelyNotify(context, conversation.recipient, conversation.notificationId, builder.build())
val notificationId: Int = if (Build.VERSION.SDK_INT < 23) NotificationIds.MESSAGE_SUMMARY else conversation.notificationId
NotificationManagerCompat.from(context).safelyNotify(context, conversation.recipient, notificationId, builder.build())
}
private fun notifySummary(context: Context, state: NotificationStateV2) {
val builder: NotificationBuilder = NotificationBuilder.create(context)
builder.apply {
setSmallIcon(R.drawable.ic_notification)
setColor(ContextCompat.getColor(context, R.color.core_ultramarine))
setCategory(NotificationCompat.CATEGORY_MESSAGE)
setGroup(DefaultMessageNotifier.NOTIFICATION_GROUP)
setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN)
setChannelId(NotificationChannels.getMessagesChannel(context))
setContentTitle(context.getString(R.string.app_name))
setContentIntent(PendingIntent.getActivity(context, 0, MainActivity.clearTop(context), 0))
setGroupSummary(true)
setSubText(context.getString(R.string.MessageNotifier_d_new_messages_in_d_conversations, state.messageCount, state.threadCount))
setContentInfo(state.messageCount.toString())
setNumber(state.messageCount)
setSummaryContentText(state.mostRecentSender)
setDeleteIntent(state.getDeleteIntent(context))
setWhen(state.mostRecentNotification)
addMarkAsReadAction(state)
addMessages(state)
setOnlyAlertOnce(!state.notificationItems.any { it.isNewNotification })
setPriority(TextSecurePreferences.getNotificationPriority(context))
setLights()
setAlarms(state.mostRecentSender)
setTicker(state.mostRecentNotification.getStyledPrimaryText(context, true))
}
Log.d(TAG, "showing summary notification")
NotificationManagerCompat.from(context).safelyNotify(context, null, NotificationIds.MESSAGE_SUMMARY, builder.build())
}
private fun notifyInThread(context: Context, recipient: Recipient, lastAudibleNotification: Long) {

Wyświetl plik

@ -39,6 +39,10 @@ data class NotificationStateV2(val conversations: List<NotificationConversation>
val mostRecentSender: Recipient
get() = mostRecentNotification.individualRecipient
fun getNonVisibleConversation(visibleThreadId: Long): List<NotificationConversation> {
return conversations.filterNot { it.threadId == visibleThreadId }
}
fun getDeleteIntent(context: Context): PendingIntent? {
val ids = LongArray(messageCount)
val mms = BooleanArray(ids.size)

Wyświetl plik

@ -343,7 +343,7 @@ public final class FeatureFlags {
/** Whether or not to use the new notification system. */
public static boolean useNewNotificationSystem() {
return getBoolean(NOTIFICATION_REWRITE, false) && Build.VERSION.SDK_INT >= 26;
return Build.VERSION.SDK_INT >= 26 || getBoolean(NOTIFICATION_REWRITE, false);
}
public static boolean mp4GifSendSupport() {

Wyświetl plik

@ -15,6 +15,7 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.uast.UCallExpression;
import org.jetbrains.uast.UExpression;
import org.jetbrains.uast.java.JavaUSimpleNameReferenceExpression;
import org.jetbrains.uast.kotlin.KotlinUQualifiedReferenceExpression;
import org.jetbrains.uast.kotlin.KotlinUSimpleReferenceExpression;
import java.util.Arrays;
@ -74,7 +75,7 @@ public final class SignalLogDetector extends Detector implements Detector.UastSc
if (evaluator.isMemberInClass(method, "org.signal.core.util.logging.Log")) {
List<UExpression> arguments = call.getValueArguments();
UExpression tag = arguments.get(0);
if (!(tag instanceof JavaUSimpleNameReferenceExpression || tag instanceof KotlinUSimpleReferenceExpression)) {
if (!(tag instanceof JavaUSimpleNameReferenceExpression || tag instanceof KotlinUSimpleReferenceExpression || tag instanceof KotlinUQualifiedReferenceExpression)) {
context.report(INLINE_TAG, call, context.getLocation(call), "Not using a tag constant");
}
}

Wyświetl plik

@ -152,6 +152,27 @@ public final class LogDetectorTest {
.expectClean();
}
@Test
public void log_uses_tag_companion_kotlin() {
lint()
.files(appLogStub,
kotlin("package foo\n" +
"import org.signal.core.util.logging.Log\n" +
"class Example {\n" +
" companion object { val TAG: String = Log.tag(Example::class.java) }\n" +
" fun log() {\n" +
" Log.d(TAG, \"msg\")\n" +
" }\n" +
"}\n"+
"fun logOutsie() {\n" +
" Log.d(Example.TAG, \"msg\")\n" +
"}\n")
)
.issues(SignalLogDetector.INLINE_TAG)
.run()
.expectClean();
}
@Test
public void log_uses_inline_tag() {
lint()