Skip to content

Commit d847439

Browse files
committed
fix(notifications): respect dedicated notification avatar setting
Closes #10750. Per @rafaeltonholo's review: MessageListPreferencesManager controls avatars on the message list, not in notifications. A user may want avatars in one place but not the other. Adds a new isShowContactPictureInNotification field on NotificationPreference, persists it via NotificationPreferenceManager, and reads it from SingleMessageNotificationCreator.setAvatar(). Adds a matching toggle in general settings. Defaults to true to preserve current behavior. Pattern mirrors commit 8dfa32e.
1 parent 2200869 commit d847439

9 files changed

Lines changed: 196 additions & 0 deletions

File tree

core/preference/api/src/commonMain/kotlin/net/thunderbird/core/preference/notification/NotificationPreference.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ val NOTIFICATION_PREFERENCE_DEFAULT_MESSAGE_ACTIONS_ORDER = NotificationActionTo
1212
const val NOTIFICATION_PREFERENCE_DEFAULT_MESSAGE_ACTIONS_CUTOFF = 3
1313
const val NOTIFICATION_PREFERENCE_MAX_MESSAGE_ACTIONS_SHOWN = 3
1414
const val NOTIFICATION_PREFERENCE_DEFAULT_IS_SUMMARY_DELETE_ACTION_ENABLED = true
15+
const val NOTIFICATION_PREFERENCE_DEFAULT_IS_SHOW_CONTACT_PICTURE_IN_NOTIFICATION = true
1516
val NOTIFICATION_PREFERENCE_DEFAULT_QUICK_DELETE_BEHAVIOUR = NotificationQuickDelete.ALWAYS
1617
val NOTIFICATION_PREFERENCE_DEFAULT_LOCK_SCREEN_NOTIFICATION_VISIBILITY = LockScreenNotificationVisibility.MESSAGE_COUNT
1718

@@ -24,6 +25,8 @@ data class NotificationPreference(
2425
val messageActionsOrder: List<String> = NOTIFICATION_PREFERENCE_DEFAULT_MESSAGE_ACTIONS_ORDER,
2526
val messageActionsCutoff: Int = NOTIFICATION_PREFERENCE_DEFAULT_MESSAGE_ACTIONS_CUTOFF,
2627
val isSummaryDeleteActionEnabled: Boolean = NOTIFICATION_PREFERENCE_DEFAULT_IS_SUMMARY_DELETE_ACTION_ENABLED,
28+
val isShowContactPictureInNotification: Boolean =
29+
NOTIFICATION_PREFERENCE_DEFAULT_IS_SHOW_CONTACT_PICTURE_IN_NOTIFICATION,
2730
val notificationQuickDeleteBehaviour: NotificationQuickDelete =
2831
NOTIFICATION_PREFERENCE_DEFAULT_QUICK_DELETE_BEHAVIOUR,
2932
val lockScreenNotificationVisibility: LockScreenNotificationVisibility =

core/preference/api/src/commonMain/kotlin/net/thunderbird/core/preference/notification/NotificationPreferenceManager.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const val KEY_NOTIFICATION_DURING_QUIET_TIME_ENABLED = "notificationDuringQuietT
99
const val KEY_MESSAGE_ACTIONS_ORDER = "messageActionsOrder"
1010
const val KEY_MESSAGE_ACTIONS_CUTOFF = "messageActionsCutoff"
1111
const val KEY_IS_SUMMARY_DELETE_ACTION_ENABLED = "isSummaryDeleteActionEnabled"
12+
const val KEY_SHOW_CONTACT_PICTURE_IN_NOTIFICATION = "showContactPictureInNotification"
1213

1314
const val KEY_NOTIFICATION_QUICK_DELETE_BEHAVIOUR = "notificationQuickDelete"
1415
const val KEY_LOCK_SCREEN_NOTIFICATION_VISIBILITY = "lockScreenNotificationVisibility"

core/preference/impl/src/commonMain/kotlin/net/thunderbird/core/preference/notification/DefaultNotificationPreferenceManager.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ class DefaultNotificationPreferenceManager(
7777
defValue = NOTIFICATION_PREFERENCE_DEFAULT_MESSAGE_ACTIONS_CUTOFF,
7878
),
7979
isSummaryDeleteActionEnabled = isSummaryDeleteActionEnabled,
80+
isShowContactPictureInNotification = storage.getBoolean(
81+
key = KEY_SHOW_CONTACT_PICTURE_IN_NOTIFICATION,
82+
defValue = NOTIFICATION_PREFERENCE_DEFAULT_IS_SHOW_CONTACT_PICTURE_IN_NOTIFICATION,
83+
),
8084
notificationQuickDeleteBehaviour = notificationQuickDeleteBehaviour,
8185
lockScreenNotificationVisibility = storage.getEnumOrDefault(
8286
key = KEY_LOCK_SCREEN_NOTIFICATION_VISIBILITY,
@@ -125,6 +129,10 @@ class DefaultNotificationPreferenceManager(
125129
)
126130
storageEditor.putInt(KEY_MESSAGE_ACTIONS_CUTOFF, config.messageActionsCutoff)
127131
storageEditor.putBoolean(KEY_IS_SUMMARY_DELETE_ACTION_ENABLED, config.isSummaryDeleteActionEnabled)
132+
storageEditor.putBoolean(
133+
KEY_SHOW_CONTACT_PICTURE_IN_NOTIFICATION,
134+
config.isShowContactPictureInNotification,
135+
)
128136
storageEditor.putEnum(
129137
KEY_NOTIFICATION_QUICK_DELETE_BEHAVIOUR,
130138
config.notificationQuickDeleteBehaviour,

legacy/core/src/main/java/com/fsck/k9/notification/CoreNotificationKoinModule.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ val coreNotificationModule = module {
112112
actionCreator = get(),
113113
resourceProvider = get(),
114114
lockScreenNotificationCreator = get(),
115+
notificationPreferenceManager = get(),
115116
application = androidApplication(),
116117
)
117118
}

legacy/core/src/main/java/com/fsck/k9/notification/SingleMessageNotificationCreator.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,15 @@ import kotlinx.coroutines.Dispatchers
1010
import kotlinx.coroutines.SupervisorJob
1111
import kotlinx.coroutines.launch
1212
import net.thunderbird.core.logging.legacy.Log
13+
import net.thunderbird.core.preference.notification.NotificationPreferenceManager
1314
import androidx.core.app.NotificationCompat.Builder as NotificationBuilder
1415

1516
internal class SingleMessageNotificationCreator(
1617
private val notificationHelper: NotificationHelper,
1718
private val actionCreator: NotificationActionCreator,
1819
private val resourceProvider: NotificationResourceProvider,
1920
private val lockScreenNotificationCreator: LockScreenNotificationCreator,
21+
private val notificationPreferenceManager: NotificationPreferenceManager,
2022
private val application: Application,
2123
) {
2224
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
@@ -63,6 +65,8 @@ internal class SingleMessageNotificationCreator(
6365
}
6466

6567
private suspend fun NotificationBuilder.setAvatar(content: NotificationContent) = apply {
68+
if (!notificationPreferenceManager.getConfig().isShowContactPictureInNotification) return@apply
69+
6670
resourceProvider.avatar(content.sender)?.let {
6771
setLargeIcon(IconCompat.createWithAdaptiveBitmap(it).toIcon(application))
6872
}
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
package com.fsck.k9.notification
2+
3+
import android.app.Application
4+
import android.app.Notification
5+
import android.app.PendingIntent
6+
import android.graphics.Bitmap
7+
import androidx.core.app.NotificationCompat
8+
import androidx.test.core.app.ApplicationProvider
9+
import app.k9mail.legacy.message.controller.MessageReference
10+
import assertk.assertThat
11+
import assertk.assertions.isEqualTo
12+
import com.fsck.k9.mail.Address
13+
import kotlinx.coroutines.flow.Flow
14+
import kotlinx.coroutines.flow.MutableStateFlow
15+
import kotlinx.coroutines.flow.update
16+
import kotlinx.coroutines.test.UnconfinedTestDispatcher
17+
import kotlinx.coroutines.test.runTest
18+
import net.thunderbird.core.android.account.LegacyAccountDto
19+
import net.thunderbird.core.android.testing.MockHelper.mockBuilder
20+
import net.thunderbird.core.android.testing.RobolectricTest
21+
import net.thunderbird.core.preference.notification.NotificationPreference
22+
import net.thunderbird.core.preference.notification.NotificationPreferenceManager
23+
import net.thunderbird.core.testing.coroutines.MainDispatcherHelper
24+
import org.junit.After
25+
import org.junit.Before
26+
import org.junit.Test
27+
import org.mockito.kotlin.any
28+
import org.mockito.kotlin.doReturn
29+
import org.mockito.kotlin.mock
30+
31+
class SingleMessageNotificationCreatorTest : RobolectricTest() {
32+
private val mainDispatcher = MainDispatcherHelper(UnconfinedTestDispatcher())
33+
private val notificationPreferenceManager = FakeNotificationPreferenceManager()
34+
private val resourceProvider = TestAvatarNotificationResourceProvider()
35+
private val notification = mock<Notification>()
36+
private val builder = mockBuilder<NotificationCompat.Builder> {
37+
on { build() } doReturn notification
38+
}
39+
40+
private lateinit var testSubject: SingleMessageNotificationCreator
41+
42+
@Before
43+
fun setUp() {
44+
mainDispatcher.setUp()
45+
testSubject = SingleMessageNotificationCreator(
46+
notificationHelper = createNotificationHelper(),
47+
actionCreator = createNotificationActionCreator(),
48+
resourceProvider = resourceProvider,
49+
lockScreenNotificationCreator = mock(),
50+
notificationPreferenceManager = notificationPreferenceManager,
51+
application = ApplicationProvider.getApplicationContext<Application>(),
52+
)
53+
}
54+
55+
@After
56+
fun tearDown() {
57+
mainDispatcher.tearDown()
58+
}
59+
60+
@Test
61+
fun `create notification looks up avatar when notification contact pictures are enabled`() = runTest {
62+
notificationPreferenceManager.setShowContactPictureInNotification(true)
63+
64+
testSubject.createSingleNotification(
65+
baseNotificationData = createBaseNotificationData(),
66+
singleNotificationData = createSingleNotificationData(),
67+
).join()
68+
69+
assertThat(resourceProvider.avatarCalls).isEqualTo(1)
70+
}
71+
72+
@Test
73+
fun `create notification skips avatar lookup when notification contact pictures are disabled`() = runTest {
74+
notificationPreferenceManager.setShowContactPictureInNotification(false)
75+
76+
testSubject.createSingleNotification(
77+
baseNotificationData = createBaseNotificationData(),
78+
singleNotificationData = createSingleNotificationData(),
79+
).join()
80+
81+
assertThat(resourceProvider.avatarCalls).isEqualTo(0)
82+
}
83+
84+
private fun createNotificationHelper(): NotificationHelper {
85+
return mock {
86+
on { createNotificationBuilder(any(), any()) } doReturn builder
87+
}
88+
}
89+
90+
private fun createNotificationActionCreator(): NotificationActionCreator {
91+
val pendingIntent = mock<PendingIntent>()
92+
return mock {
93+
on { createViewMessagePendingIntent(any()) } doReturn pendingIntent
94+
on { createDismissMessagePendingIntent(any()) } doReturn pendingIntent
95+
}
96+
}
97+
98+
private fun createBaseNotificationData(): BaseNotificationData {
99+
return BaseNotificationData(
100+
account = LegacyAccountDto("00000000-0000-0000-0000-000000000000"),
101+
accountName = "Account name",
102+
groupKey = "group",
103+
color = 0,
104+
newMessagesCount = 1,
105+
lockScreenNotificationData = LockScreenNotificationData.None,
106+
appearance = NotificationAppearance(
107+
ringtone = null,
108+
vibrationPattern = null,
109+
ledColor = null,
110+
),
111+
)
112+
}
113+
114+
private fun createSingleNotificationData(): SingleNotificationData {
115+
return SingleNotificationData(
116+
notificationId = 23,
117+
isSilent = true,
118+
timestamp = 9000,
119+
content = NotificationContent(
120+
messageReference = MessageReference("account", 1, "uid"),
121+
sender = Address("alice@example.com", "Alice"),
122+
subject = "Subject",
123+
preview = "Preview",
124+
summary = "Summary",
125+
),
126+
actions = emptyList(),
127+
wearActions = emptyList(),
128+
addLockScreenNotification = false,
129+
)
130+
}
131+
132+
private class TestAvatarNotificationResourceProvider :
133+
NotificationResourceProvider by TestNotificationResourceProvider() {
134+
var avatarCalls = 0
135+
136+
override suspend fun avatar(address: Address): Bitmap? {
137+
avatarCalls += 1
138+
return Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)
139+
}
140+
}
141+
142+
private class FakeNotificationPreferenceManager : NotificationPreferenceManager {
143+
private val prefs = MutableStateFlow(NotificationPreference())
144+
145+
override fun save(config: NotificationPreference) = Unit
146+
147+
override fun getConfig(): NotificationPreference = prefs.value
148+
149+
override fun getConfigFlow(): Flow<NotificationPreference> = prefs
150+
151+
fun setShowContactPictureInNotification(isEnabled: Boolean) {
152+
prefs.update { it.copy(isShowContactPictureInNotification = isEnabled) }
153+
}
154+
}
155+
}

legacy/ui/legacy/src/main/java/com/fsck/k9/ui/settings/general/GeneralSettingsDataStore.kt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ class GeneralSettingsDataStore(
6161
"quiet_time_enabled" -> notificationSettings.isQuietTimeEnabled
6262
"disable_notifications_during_quiet_time" -> !notificationSettings.isNotificationDuringQuietTimeEnabled
6363
"notification_summary_delete" -> notificationSettings.isSummaryDeleteActionEnabled
64+
"notification_show_contact_picture" -> notificationSettings.isShowContactPictureInNotification
6465
"privacy_hide_useragent" -> privacySettings.isHideUserAgent
6566
"privacy_hide_timezone" -> privacySettings.isHideTimeZone
6667
"debug_logging" -> debuggingSettings.isDebugLoggingEnabled
@@ -103,6 +104,10 @@ class GeneralSettingsDataStore(
103104
"quiet_time_enabled" -> setIsQuietTimeEnabled(isQuietTimeEnabled = value)
104105
"disable_notifications_during_quiet_time" -> setIsNotificationDuringQuietTimeEnabled(!value)
105106
"notification_summary_delete" -> setIsSummaryDeleteActionEnabled(isSummaryDeleteActionEnabled = value)
107+
"notification_show_contact_picture" -> setIsShowContactPictureInNotification(
108+
isShowContactPictureInNotification = value,
109+
)
110+
106111
"privacy_hide_useragent" -> setIsHideUserAgent(isHideUserAgent = value)
107112
"privacy_hide_timezone" -> setIsHideTimeZone(isHideTimeZone = value)
108113
"debug_logging" -> setIsDebugLoggingEnabled(isDebugLoggingEnabled = value)
@@ -670,6 +675,17 @@ class GeneralSettingsDataStore(
670675
}
671676
}
672677

678+
private fun setIsShowContactPictureInNotification(isShowContactPictureInNotification: Boolean) {
679+
skipSaveSettings = true
680+
generalSettingsManager.update { settings ->
681+
settings.copy(
682+
notification = settings.notification.copy(
683+
isShowContactPictureInNotification = isShowContactPictureInNotification,
684+
),
685+
)
686+
}
687+
}
688+
673689
private fun setIsHideTimeZone(isHideTimeZone: Boolean) {
674690
skipSaveSettings = true
675691
generalSettingsManager.update { settings ->

legacy/ui/legacy/src/main/res/values/strings.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,8 @@
145145
<string name="notification_actions_settings_summary">Choose which actions appear in email notifications</string>
146146
<string name="notification_summary_delete_title">Show Delete on multi-message notifications</string>
147147
<string name="notification_summary_delete_summary">Adds a Delete action to notifications that summarize multiple new messages</string>
148+
<string name="notification_show_contact_picture_title">Show contact pictures in notifications</string>
149+
<string name="notification_show_contact_picture_summary">Display sender avatars in new message notifications</string>
148150
<string name="notification_actions_settings_description">Reorder notification actions.</string>
149151
<string name="notification_actions_cutoff_description">Shown actions cutoff</string>
150152
<string name="notification_actions_cutoff_state">Actions above this line are shown in the notification</string>

legacy/ui/legacy/src/main/res/xml/general_settings.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -505,6 +505,12 @@
505505
android:summary="@string/notification_summary_delete_summary"
506506
/>
507507

508+
<SwitchPreferenceCompat
509+
android:key="notification_show_contact_picture"
510+
android:title="@string/notification_show_contact_picture_title"
511+
android:summary="@string/notification_show_contact_picture_summary"
512+
/>
513+
508514
</PreferenceScreen>
509515

510516
<PreferenceScreen

0 commit comments

Comments
 (0)