Skip to content

Commit 5c853f9

Browse files
authored
Merge pull request #17343 from wordpress-mobile/issue/17328-blogging-reminders-local-notifications
[Blogging Reminders Sync] Implement blogging reminders local notification schedule
2 parents 5b7484e + 5c80c62 commit 5c853f9

8 files changed

Lines changed: 156 additions & 47 deletions

File tree

WordPress/src/main/java/org/wordpress/android/bloggingreminders/provider/BloggingRemindersProvider.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import org.wordpress.android.util.publicdata.ClientVerification
1515
import org.wordpress.android.util.signature.SignatureNotFoundException
1616
import javax.inject.Inject
1717

18-
typealias SiteIDBloggingReminderMap = Map<Long?, BloggingRemindersModel?>
18+
typealias RemoteSiteId = Long
1919

2020
class BloggingRemindersProvider : QueryContentProvider() {
2121
@Inject lateinit var bloggingRemindersStore: BloggingRemindersStore
@@ -53,7 +53,9 @@ class BloggingRemindersProvider : QueryContentProvider() {
5353
val filteredSiteIds = filteredBloggingReminders.map { bloggingReminder ->
5454
siteStore.getSiteIdForLocalId(bloggingReminder.siteId)
5555
}
56-
val result: SiteIDBloggingReminderMap = filteredSiteIds.zip(filteredBloggingReminders).toMap()
56+
val result: Map<RemoteSiteId?, BloggingRemindersModel?> = filteredSiteIds.zip(
57+
filteredBloggingReminders
58+
).toMap()
5759
queryResult.createCursor(result)
5860
}
5961
} else null

WordPress/src/main/java/org/wordpress/android/bloggingreminders/resolver/BloggingRemindersResolver.kt

Lines changed: 57 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,24 @@ import org.wordpress.android.bloggingreminders.BloggingRemindersSyncAnalyticsTra
99
import org.wordpress.android.bloggingreminders.BloggingRemindersSyncAnalyticsTracker.ErrorType
1010
import org.wordpress.android.bloggingreminders.JetpackBloggingRemindersSyncFlag
1111
import org.wordpress.android.bloggingreminders.provider.BloggingRemindersProvider
12-
import org.wordpress.android.bloggingreminders.provider.SiteIDBloggingReminderMap
12+
import org.wordpress.android.bloggingreminders.provider.RemoteSiteId
13+
import org.wordpress.android.fluxc.model.BloggingRemindersModel
1314
import org.wordpress.android.fluxc.store.BloggingRemindersStore
1415
import org.wordpress.android.fluxc.store.SiteStore
1516
import org.wordpress.android.modules.APPLICATION_SCOPE
1617
import org.wordpress.android.provider.query.QueryResult
1718
import org.wordpress.android.resolver.ContentResolverWrapper
19+
import org.wordpress.android.ui.bloggingreminders.BloggingRemindersModelMapper
1820
import org.wordpress.android.ui.prefs.AppPrefsWrapper
21+
import org.wordpress.android.util.extensions.filterNull
1922
import org.wordpress.android.util.publicdata.WordPressPublicData
2023
import org.wordpress.android.viewmodel.ContextProvider
24+
import org.wordpress.android.workers.reminder.ReminderScheduler
2125
import javax.inject.Inject
2226
import javax.inject.Named
2327

28+
typealias RemoteSiteIdBloggingRemindersMap = Map<RemoteSiteId, BloggingRemindersModel>
29+
2430
class BloggingRemindersResolver @Inject constructor(
2531
private val jetpackBloggingRemindersSyncFlag: JetpackBloggingRemindersSyncFlag,
2632
private val contextProvider: ContextProvider,
@@ -31,28 +37,20 @@ class BloggingRemindersResolver @Inject constructor(
3137
private val bloggingRemindersSyncAnalyticsTracker: BloggingRemindersSyncAnalyticsTracker,
3238
private val siteStore: SiteStore,
3339
private val bloggingRemindersStore: BloggingRemindersStore,
34-
@Named(APPLICATION_SCOPE) private val coroutineScope: CoroutineScope
40+
@Named(APPLICATION_SCOPE) private val coroutineScope: CoroutineScope,
41+
private val reminderScheduler: ReminderScheduler,
42+
private val bloggingRemindersModelMapper: BloggingRemindersModelMapper
3543
) {
3644
fun trySyncBloggingReminders(onSuccess: () -> Unit, onFailure: () -> Unit) {
37-
val isFeatureFlagEnabled = jetpackBloggingRemindersSyncFlag.isEnabled()
38-
if (!isFeatureFlagEnabled) {
39-
onFailure()
40-
return
41-
}
42-
val isFirstTry = appPrefsWrapper.getIsFirstTryBloggingRemindersSyncJetpack()
43-
if (!isFirstTry) {
45+
if (!shouldTrySyncBloggingReminders()) {
4446
onFailure()
4547
return
4648
}
47-
bloggingRemindersSyncAnalyticsTracker.trackStart()
48-
appPrefsWrapper.saveIsFirstTryBloggingRemindersSyncJetpack(false)
4949
val bloggingRemindersResultCursor = getBloggingRemindersSyncResultCursor()
5050
if (bloggingRemindersResultCursor != null) {
51-
val siteModelBloggingReminderMap = queryResult.getValue<SiteIDBloggingReminderMap>(
52-
bloggingRemindersResultCursor, object : TypeToken<SiteIDBloggingReminderMap?>() {}.type
53-
) ?: emptyMap()
54-
if (siteModelBloggingReminderMap.isNotEmpty()) {
55-
val success = syncBloggingReminders(siteModelBloggingReminderMap)
51+
val remindersMap = mapBloggingRemindersResultCursor(bloggingRemindersResultCursor).filterNull()
52+
if (remindersMap.isNotEmpty()) {
53+
val success = setBloggingReminders(remindersMap)
5654
if (success) onSuccess() else onFailure()
5755
} else {
5856
bloggingRemindersSyncAnalyticsTracker.trackSuccess(0)
@@ -64,6 +62,27 @@ class BloggingRemindersResolver @Inject constructor(
6462
}
6563
}
6664

65+
private fun mapBloggingRemindersResultCursor(bloggingRemindersResultCursor: Cursor) =
66+
queryResult.getValue<Map<RemoteSiteId?, BloggingRemindersModel?>>(
67+
bloggingRemindersResultCursor,
68+
object : TypeToken<Map<RemoteSiteId?, BloggingRemindersModel?>>() {}.type
69+
) ?: emptyMap()
70+
71+
@Suppress("ReturnCount")
72+
private fun shouldTrySyncBloggingReminders(): Boolean {
73+
val isFeatureFlagEnabled = jetpackBloggingRemindersSyncFlag.isEnabled()
74+
if (!isFeatureFlagEnabled) {
75+
return false
76+
}
77+
val isFirstTry = appPrefsWrapper.getIsFirstTryBloggingRemindersSyncJetpack()
78+
if (!isFirstTry) {
79+
return false
80+
}
81+
bloggingRemindersSyncAnalyticsTracker.trackStart()
82+
appPrefsWrapper.saveIsFirstTryBloggingRemindersSyncJetpack(false)
83+
return true
84+
}
85+
6786
private fun getBloggingRemindersSyncResultCursor(): Cursor? {
6887
val wordpressBloggingRemindersSyncUriValue =
6988
"content://${wordPressPublicData.currentPackageId()}.${BloggingRemindersProvider::class.simpleName}"
@@ -74,28 +93,38 @@ class BloggingRemindersResolver @Inject constructor(
7493
}
7594

7695
@Suppress("TooGenericExceptionCaught", "SwallowedException")
77-
private fun syncBloggingReminders(siteIdBloggingReminderMap: SiteIDBloggingReminderMap): Boolean {
96+
private fun setBloggingReminders(remindersMap: RemoteSiteIdBloggingRemindersMap): Boolean {
7897
try {
7998
coroutineScope.launch {
80-
var remindersSyncedCount = 0
81-
for ((siteId, bloggingReminder) in siteIdBloggingReminderMap) {
82-
if (siteId == null || bloggingReminder == null) {
83-
continue
84-
}
99+
var syncCount = 0
100+
for ((siteId, bloggingReminder) in remindersMap) {
85101
val siteLocalId = siteStore.getLocalIdForRemoteSiteId(siteId)
86-
val isBloggingReminderAlreadySet = bloggingRemindersStore.bloggingRemindersModel(siteLocalId)
87-
.first().enabledDays.isNotEmpty()
88-
if (siteLocalId != 0 && !isBloggingReminderAlreadySet) {
89-
remindersSyncedCount = remindersSyncedCount.inc()
90-
bloggingRemindersStore.updateBloggingReminders(bloggingReminder.copy(siteId = siteLocalId))
102+
if (siteLocalId != 0 && !isBloggingReminderAlreadySet(siteLocalId)) {
103+
val bloggingReminderWithLocalId = bloggingReminder.copy(siteId = siteLocalId)
104+
bloggingRemindersStore.updateBloggingReminders(bloggingReminderWithLocalId)
105+
setLocalReminderNotification(bloggingReminderWithLocalId)
106+
syncCount = syncCount.inc()
91107
}
92108
}
93-
bloggingRemindersSyncAnalyticsTracker.trackSuccess(remindersSyncedCount)
109+
bloggingRemindersSyncAnalyticsTracker.trackSuccess(syncCount)
94110
}
95111
return true
96112
} catch (exception: Exception) {
97113
bloggingRemindersSyncAnalyticsTracker.trackFailed(ErrorType.UpdateBloggingRemindersError)
98114
return false
99115
}
100116
}
117+
118+
private suspend fun isBloggingReminderAlreadySet(siteLocalId: Int) =
119+
bloggingRemindersStore.bloggingRemindersModel(siteLocalId).first().enabledDays.isNotEmpty()
120+
121+
private fun setLocalReminderNotification(bloggingRemindersModel: BloggingRemindersModel) {
122+
val bloggingRemindersUiModel = bloggingRemindersModelMapper.toUiModel(bloggingRemindersModel)
123+
reminderScheduler.schedule(
124+
bloggingRemindersUiModel.siteId,
125+
bloggingRemindersUiModel.hour,
126+
bloggingRemindersUiModel.minute,
127+
bloggingRemindersUiModel.toReminderConfig()
128+
)
129+
}
101130
}

WordPress/src/main/java/org/wordpress/android/sharedlogin/resolver/SharedLoginResolver.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ class SharedLoginResolver @Inject constructor(
4747
appPrefsWrapper.saveIsFirstTrySharedLoginJetpack(false)
4848
val accessTokenCursor = getAccessTokenCursor()
4949
if (accessTokenCursor != null) {
50-
val accessToken = queryResult.getValue<String>(accessTokenCursor) ?: ""
50+
val accessToken = queryResult.getValue(accessTokenCursor) ?: ""
5151
if (accessToken.isNotEmpty()) {
5252
sharedLoginAnalyticsTracker.trackLoginSuccess()
5353
userFlagsResolver.tryGetUserFlags(

WordPress/src/main/java/org/wordpress/android/ui/bloggingreminders/BloggingRemindersUiModel.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.wordpress.android.ui.bloggingreminders
22

3+
import org.wordpress.android.workers.reminder.ReminderConfig.WeeklyReminder
34
import java.time.DayOfWeek
45
import java.time.LocalTime
56
import java.time.format.DateTimeFormatter
@@ -18,4 +19,6 @@ data class BloggingRemindersUiModel(
1819

1920
fun getNotificationTime24hour(): CharSequence =
2021
LocalTime.of(hour, minute).format(DateTimeFormatter.ofPattern("HH:mm", Locale.ROOT))
22+
23+
fun toReminderConfig() = WeeklyReminder(this.enabledDays)
2124
}

WordPress/src/main/java/org/wordpress/android/ui/bloggingreminders/BloggingRemindersViewModel.kt

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ import org.wordpress.android.util.merge
2323
import org.wordpress.android.util.perform
2424
import org.wordpress.android.viewmodel.Event
2525
import org.wordpress.android.viewmodel.ScopedViewModel
26-
import org.wordpress.android.workers.reminder.ReminderConfig.WeeklyReminder
2726
import org.wordpress.android.workers.reminder.ReminderScheduler
2827
import java.time.DayOfWeek
2928
import javax.inject.Inject
@@ -280,9 +279,6 @@ class BloggingRemindersViewModel @Inject constructor(
280279
}
281280
}
282281

283-
private fun BloggingRemindersUiModel.toReminderConfig() =
284-
WeeklyReminder(this.enabledDays)
285-
286282
enum class Screen(val trackingName: String) {
287283
PROLOGUE("main"), // displayed after post is published
288284
PROLOGUE_SETTINGS("main"), // displayed from Site Settings before showing cadence selector
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package org.wordpress.android.util.extensions
2+
3+
@Suppress("UNCHECKED_CAST")
4+
fun <K, V> Map<K?, V?>.filterNull(): Map<K, V> =
5+
(filterValues { it != null } as Map<K?, V>)
6+
.filterKeys { it != null } as Map<K, V>

WordPress/src/test/java/org/wordpress/android/bloggingreminders/resolver/BloggingRemindersResolverTest.kt

Lines changed: 70 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,13 @@ import org.wordpress.android.fluxc.store.SiteStore
2626
import org.wordpress.android.provider.query.QueryResult
2727
import org.wordpress.android.resolver.ContentResolverWrapper
2828
import org.wordpress.android.test
29+
import org.wordpress.android.ui.bloggingreminders.BloggingRemindersModelMapper
30+
import org.wordpress.android.ui.bloggingreminders.BloggingRemindersUiModel
2931
import org.wordpress.android.ui.prefs.AppPrefsWrapper
3032
import org.wordpress.android.util.publicdata.WordPressPublicData
3133
import org.wordpress.android.viewmodel.ContextProvider
34+
import org.wordpress.android.workers.reminder.ReminderScheduler
35+
import java.time.DayOfWeek
3236

3337
@ExperimentalCoroutinesApi
3438
class BloggingRemindersResolverTest {
@@ -44,6 +48,8 @@ class BloggingRemindersResolverTest {
4448
private val bloggingRemindersSyncAnalyticsTracker: BloggingRemindersSyncAnalyticsTracker = mock()
4549
private val siteStore: SiteStore = mock()
4650
private val bloggingRemindersStore: BloggingRemindersStore = mock()
51+
private val reminderScheduler: ReminderScheduler = mock()
52+
private val bloggingRemindersModelMapper: BloggingRemindersModelMapper = mock()
4753
private val classToTest = BloggingRemindersResolver(
4854
jetpackBloggingRemindersSyncFlag,
4955
contextProvider,
@@ -54,7 +60,9 @@ class BloggingRemindersResolverTest {
5460
bloggingRemindersSyncAnalyticsTracker,
5561
siteStore,
5662
bloggingRemindersStore,
57-
coroutineScope
63+
coroutineScope,
64+
reminderScheduler,
65+
bloggingRemindersModelMapper
5866
)
5967

6068
private val context: Context = mock()
@@ -63,8 +71,11 @@ class BloggingRemindersResolverTest {
6371
private val wordPressCurrentPackageId = "packageId"
6472
private val uriValue = "content://$wordPressCurrentPackageId.${BloggingRemindersProvider::class.simpleName}"
6573
private val validLocalId = 123
66-
private val userSetBloggingRemindersModel = BloggingRemindersModel(validLocalId, setOf(MONDAY))
74+
private val userSetBloggingRemindersModel = BloggingRemindersModel(validLocalId, setOf(MONDAY), 5, 43, false)
6775
private val defaultBloggingRemindersModel = BloggingRemindersModel(validLocalId)
76+
private val bloggingRemindersUiModel = BloggingRemindersUiModel(
77+
validLocalId, setOf(DayOfWeek.MONDAY), 5, 43, false
78+
)
6879

6980
@Before
7081
fun setup() {
@@ -164,12 +175,15 @@ class BloggingRemindersResolverTest {
164175

165176
@Test
166177
fun `Should track success if result map has entries`() = test {
178+
whenever(bloggingRemindersModelMapper.toUiModel(any())).thenReturn(bloggingRemindersUiModel)
167179
whenever(siteStore.getLocalIdForRemoteSiteId(123L)).thenReturn(validLocalId)
168180
whenever(bloggingRemindersStore.bloggingRemindersModel(validLocalId))
169181
.thenReturn(flowOf(defaultBloggingRemindersModel))
170182
featureEnabled()
171-
whenever(mockCursor.getString(0)).thenReturn("{\"123\":{\"enabledDays\":[\"MONDAY\"],\"hour\":5" +
172-
",\"isPromptIncluded\":false,\"minute\":43,\"siteId\":123}}")
183+
whenever(mockCursor.getString(0)).thenReturn(
184+
"{\"123\":{\"enabledDays\":[\"MONDAY\"],\"hour\":5" +
185+
",\"isPromptIncluded\":false,\"minute\":43,\"siteId\":123}}"
186+
)
173187
classToTest.trySyncBloggingReminders({}, {})
174188
verify(bloggingRemindersSyncAnalyticsTracker).trackSuccess(1)
175189
}
@@ -180,21 +194,26 @@ class BloggingRemindersResolverTest {
180194
.thenReturn(flowOf(userSetBloggingRemindersModel))
181195
whenever(siteStore.getLocalIdForRemoteSiteId(123L)).thenReturn(validLocalId)
182196
featureEnabled()
183-
whenever(mockCursor.getString(0)).thenReturn("{\"123\":{\"enabledDays\":[\"MONDAY\"],\"hour\":5" +
184-
",\"isPromptIncluded\":false,\"minute\":43,\"siteId\":123}}")
197+
whenever(mockCursor.getString(0)).thenReturn(
198+
"{\"123\":{\"enabledDays\":[\"MONDAY\"],\"hour\":5" +
199+
",\"isPromptIncluded\":false,\"minute\":43,\"siteId\":123}}"
200+
)
185201
val onSuccess: () -> Unit = mock()
186202
classToTest.trySyncBloggingReminders(onSuccess) {}
187203
verify(onSuccess).invoke()
188204
}
189205

190206
@Test
191207
fun `Should update blogging reminder if site local ID is valid AND store returns default reminder`() = test {
208+
whenever(bloggingRemindersModelMapper.toUiModel(any())).thenReturn(bloggingRemindersUiModel)
192209
whenever(siteStore.getLocalIdForRemoteSiteId(123)).thenReturn(validLocalId)
193210
whenever(bloggingRemindersStore.bloggingRemindersModel(validLocalId))
194211
.thenReturn(flowOf(defaultBloggingRemindersModel))
195212
featureEnabled()
196-
whenever(mockCursor.getString(0)).thenReturn("{\"123\":{\"enabledDays\":[\"MONDAY\"],\"hour\":5" +
197-
",\"isPromptIncluded\":false,\"minute\":43,\"siteId\":123}}")
213+
whenever(mockCursor.getString(0)).thenReturn(
214+
"{\"123\":{\"enabledDays\":[\"MONDAY\"],\"hour\":5" +
215+
",\"isPromptIncluded\":false,\"minute\":43,\"siteId\":123}}"
216+
)
198217
classToTest.trySyncBloggingReminders({}, {})
199218
verify(bloggingRemindersStore, times(1)).updateBloggingReminders(
200219
BloggingRemindersModel(
@@ -207,15 +226,52 @@ class BloggingRemindersResolverTest {
207226
)
208227
}
209228

229+
@Test
230+
fun `Should map blogging reminder when setting local notification`() = test {
231+
whenever(bloggingRemindersModelMapper.toUiModel(any())).thenReturn(bloggingRemindersUiModel)
232+
whenever(siteStore.getLocalIdForRemoteSiteId(123)).thenReturn(validLocalId)
233+
whenever(bloggingRemindersStore.bloggingRemindersModel(validLocalId))
234+
.thenReturn(flowOf(defaultBloggingRemindersModel))
235+
featureEnabled()
236+
whenever(mockCursor.getString(0)).thenReturn(
237+
"{\"123\":{\"enabledDays\":[\"MONDAY\"],\"hour\":5" +
238+
",\"isPromptIncluded\":false,\"minute\":43,\"siteId\":123}}"
239+
)
240+
classToTest.trySyncBloggingReminders({}, {})
241+
verify(bloggingRemindersModelMapper).toUiModel(userSetBloggingRemindersModel)
242+
}
243+
244+
@Test
245+
fun `Should schedule blogging reminder local notification`() = test {
246+
whenever(bloggingRemindersModelMapper.toUiModel(any())).thenReturn(bloggingRemindersUiModel)
247+
whenever(siteStore.getLocalIdForRemoteSiteId(123)).thenReturn(validLocalId)
248+
whenever(bloggingRemindersStore.bloggingRemindersModel(validLocalId))
249+
.thenReturn(flowOf(defaultBloggingRemindersModel))
250+
featureEnabled()
251+
whenever(mockCursor.getString(0)).thenReturn(
252+
"{\"123\":{\"enabledDays\":[\"MONDAY\"],\"hour\":5" +
253+
",\"isPromptIncluded\":false,\"minute\":43,\"siteId\":123}}"
254+
)
255+
classToTest.trySyncBloggingReminders({}, {})
256+
verify(reminderScheduler).schedule(
257+
validLocalId,
258+
bloggingRemindersUiModel.hour,
259+
bloggingRemindersUiModel.minute,
260+
bloggingRemindersUiModel.toReminderConfig()
261+
)
262+
}
263+
210264
@Test
211265
fun `Should NOT update blogging reminder if site local ID is invalid`() = test {
212266
val invalidLocalId = 0
213267
whenever(bloggingRemindersStore.bloggingRemindersModel(invalidLocalId))
214268
.thenReturn(flowOf(defaultBloggingRemindersModel))
215269
whenever(siteStore.getLocalIdForRemoteSiteId(123)).thenReturn(invalidLocalId)
216270
featureEnabled()
217-
whenever(mockCursor.getString(0)).thenReturn("{\"123\":{\"enabledDays\":[\"MONDAY\"],\"hour\":5" +
218-
",\"isPromptIncluded\":false,\"minute\":43,\"siteId\":123}}")
271+
whenever(mockCursor.getString(0)).thenReturn(
272+
"{\"123\":{\"enabledDays\":[\"MONDAY\"],\"hour\":5" +
273+
",\"isPromptIncluded\":false,\"minute\":43,\"siteId\":123}}"
274+
)
219275
classToTest.trySyncBloggingReminders({}, {})
220276
verify(bloggingRemindersStore, times(0)).updateBloggingReminders(any())
221277
}
@@ -226,8 +282,10 @@ class BloggingRemindersResolverTest {
226282
.thenReturn(flowOf(userSetBloggingRemindersModel))
227283
whenever(siteStore.getLocalIdForRemoteSiteId(123)).thenReturn(validLocalId)
228284
featureEnabled()
229-
whenever(mockCursor.getString(0)).thenReturn("{\"123\":{\"enabledDays\":[],\"hour\":5" +
230-
",\"isPromptIncluded\":false,\"minute\":43,\"siteId\":123}}")
285+
whenever(mockCursor.getString(0)).thenReturn(
286+
"{\"123\":{\"enabledDays\":[],\"hour\":5" +
287+
",\"isPromptIncluded\":false,\"minute\":43,\"siteId\":123}}"
288+
)
231289
classToTest.trySyncBloggingReminders({}, {})
232290
verify(bloggingRemindersStore, times(0)).updateBloggingReminders(any())
233291
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package org.wordpress.android.util.extensions
2+
3+
import org.assertj.core.api.Assertions.assertThat
4+
import org.junit.Test
5+
6+
class MapExtensionsKtTest {
7+
@Test
8+
fun `Should filter null keys and values in filterNull`() {
9+
mapOf(0 to "0", null to "1", 2 to null, 3 to "3", null to null)
10+
.filterNull().forEach { (key, value) ->
11+
assertThat(key).isNotNull
12+
assertThat(value).isNotNull
13+
}
14+
}
15+
}

0 commit comments

Comments
 (0)