Skip to content

Commit abc636c

Browse files
authored
Merge pull request #17290 from wordpress-mobile/issue/reader-saved-posts-sync
[Jetpack Focus] Implement reader saved posts ContentProvider and Resolver
2 parents e65b439 + 6d74cd9 commit abc636c

File tree

18 files changed

+337
-7
lines changed

18 files changed

+337
-7
lines changed

WordPress/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ android {
123123
buildConfigField "boolean", "JETPACK_SHARED_LOGIN", "false"
124124
buildConfigField "boolean", "JETPACK_LOCAL_USER_FLAGS", "false"
125125
buildConfigField "boolean", "JETPACK_BLOGGING_REMINDERS_SYNC", "false"
126+
buildConfigField "boolean", "JETPACK_READER_SAVED_POSTS", "false"
126127
buildConfigField "boolean", "JETPACK_PROVIDER_SYNC", "false"
127128

128129
// Override these constants in jetpack product flavor to enable/ disable features
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package org.wordpress.android.datasets.wrappers
2+
3+
import dagger.Reusable
4+
import org.wordpress.android.datasets.ReaderDatabase
5+
import javax.inject.Inject
6+
7+
@Reusable
8+
class ReaderDatabaseWrapper @Inject constructor() {
9+
fun reset(retainBookmarkedPosts: Boolean) = ReaderDatabase.reset(retainBookmarkedPosts)
10+
}

WordPress/src/main/java/org/wordpress/android/datasets/wrappers/ReaderPostTableWrapper.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,7 @@ class ReaderPostTableWrapper @Inject constructor() {
2929
ReaderPostTable.getPostsWithTag(readerTag, maxRows, excludeTextColumn)
3030

3131
fun getNumPostsWithTag(readerTag: ReaderTag): Int = ReaderPostTable.getNumPostsWithTag(readerTag)
32+
33+
fun addOrUpdatePosts(readerTag: ReaderTag, posts: ReaderPostList) =
34+
ReaderPostTable.addOrUpdatePosts(readerTag, posts)
3235
}

WordPress/src/main/java/org/wordpress/android/datasets/wrappers/ReaderTagTableWrapper.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,8 @@ class ReaderTagTableWrapper @Inject constructor() {
1616
fun getFollowedTags(): ReaderTagList = ReaderTagTable.getFollowedTags()
1717

1818
fun clearTagLastUpdated(readerTag: ReaderTag) = ReaderTagTable.clearTagLastUpdated(readerTag)
19+
20+
fun addOrUpdateTag(readerTag: ReaderTag) = ReaderTagTable.addOrUpdateTag(readerTag)
21+
22+
fun getBookmarkTags(): ReaderTagList? = ReaderTagTable.getBookmarkTags()
1923
}

WordPress/src/main/java/org/wordpress/android/modules/AppComponent.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import org.wordpress.android.push.GCMMessageService;
77
import org.wordpress.android.push.GCMRegistrationIntentService;
88
import org.wordpress.android.push.NotificationsProcessingService;
9+
import org.wordpress.android.reader.savedposts.provider.ReaderSavedPostsProvider;
910
import org.wordpress.android.sharedlogin.provider.SharedLoginProvider;
1011
import org.wordpress.android.ui.AddQuickPressShortcutActivity;
1112
import org.wordpress.android.ui.CommentFullScreenDialogFragment;
@@ -655,4 +656,6 @@ public interface AppComponent {
655656
void inject(UserFlagsProvider object);
656657

657658
void inject(BloggingRemindersProvider object);
659+
660+
void inject(ReaderSavedPostsProvider object);
658661
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package org.wordpress.android.reader.savedposts
2+
3+
import org.wordpress.android.util.BuildConfigWrapper
4+
import org.wordpress.android.util.config.JetpackReaderSavedPostsFeatureConfig
5+
import javax.inject.Inject
6+
7+
class JetpackReaderSavedPostsFlag @Inject constructor(
8+
private val jetpackReaderSavedPostsFeatureConfig: JetpackReaderSavedPostsFeatureConfig,
9+
private val buildConfigWrapper: BuildConfigWrapper
10+
) {
11+
fun isEnabled() = jetpackReaderSavedPostsFeatureConfig.isEnabled() && buildConfigWrapper.isJetpackApp
12+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package org.wordpress.android.reader.savedposts
2+
3+
import org.wordpress.android.analytics.AnalyticsTracker.Stat
4+
import org.wordpress.android.reader.savedposts.ReaderSavedPostsAnalyticsTracker.ErrorType.Companion.ERROR_TYPE
5+
import org.wordpress.android.reader.savedposts.ReaderSavedPostsAnalyticsTracker.ErrorType.Companion.EXPORTED_POSTS
6+
import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper
7+
import javax.inject.Inject
8+
9+
class ReaderSavedPostsAnalyticsTracker @Inject constructor(
10+
private val analyticsTracker: AnalyticsTrackerWrapper
11+
) {
12+
fun trackStart() = analyticsTracker.track(Stat.READER_SAVED_POSTS_START)
13+
14+
fun trackSuccess(numPosts: Int) = analyticsTracker.track(
15+
Stat.READER_SAVED_POSTS_SUCCESS, mapOf(EXPORTED_POSTS to numPosts)
16+
)
17+
18+
fun trackFailed(errorType: ErrorType) =
19+
analyticsTracker.track(Stat.READER_SAVED_POSTS_FAILED, mapOf(ERROR_TYPE to errorType.value))
20+
21+
sealed class ErrorType(open val value: String) {
22+
object QuerySavedPostsError : ErrorType("query_saved_posts_error")
23+
24+
class GenericError(errorMessage: String?) : ErrorType(
25+
"generic_error: ${errorMessage?.take(EXCEPTION_MESSAGE_MAX_LENGTH) ?: ""}"
26+
)
27+
28+
companion object {
29+
const val ERROR_TYPE = "error_type"
30+
const val EXPORTED_POSTS = "exported_posts"
31+
const val EXCEPTION_MESSAGE_MAX_LENGTH = 100
32+
}
33+
}
34+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package org.wordpress.android.reader.savedposts.provider
2+
3+
import android.database.Cursor
4+
import android.net.Uri
5+
import org.wordpress.android.WordPress
6+
import org.wordpress.android.datasets.wrappers.ReaderPostTableWrapper
7+
import org.wordpress.android.models.ReaderTag
8+
import org.wordpress.android.models.ReaderTagType.BOOKMARKED
9+
import org.wordpress.android.provider.query.QueryContentProvider
10+
import org.wordpress.android.provider.query.QueryResult
11+
import org.wordpress.android.util.config.JetpackProviderSyncFeatureConfig
12+
import org.wordpress.android.util.publicdata.ClientVerification
13+
import org.wordpress.android.util.publicdata.JetpackPublicData
14+
import org.wordpress.android.util.signature.SignatureNotFoundException
15+
import javax.inject.Inject
16+
17+
class ReaderSavedPostsProvider : QueryContentProvider() {
18+
@Inject lateinit var queryResult: QueryResult
19+
@Inject lateinit var readerPostTableWrapper: ReaderPostTableWrapper
20+
@Inject lateinit var jetpackPublicData: JetpackPublicData
21+
@Inject lateinit var clientVerification: ClientVerification
22+
@Inject lateinit var jetpackProviderSyncFeatureConfig: JetpackProviderSyncFeatureConfig
23+
24+
override fun onCreate(): Boolean {
25+
return true
26+
}
27+
28+
@Suppress("SwallowedException")
29+
override fun query(
30+
uri: Uri,
31+
projection: Array<out String>?,
32+
selection: String?,
33+
selectionArgs: Array<out String>?,
34+
sortOrder: String?
35+
): Cursor? {
36+
inject()
37+
if (!jetpackProviderSyncFeatureConfig.isEnabled()) {
38+
return null
39+
}
40+
return context?.let {
41+
try {
42+
if (clientVerification.canTrust(callingPackage)) {
43+
val posts = readerPostTableWrapper.getPostsWithTag(
44+
readerTag = ReaderTag("", "", "", "", BOOKMARKED),
45+
maxRows = 0,
46+
excludeTextColumn = false
47+
)
48+
queryResult.createCursor(posts)
49+
} else null
50+
} catch (signatureNotFoundException: SignatureNotFoundException) {
51+
null
52+
}
53+
}
54+
}
55+
56+
private fun inject() {
57+
if (!this::queryResult.isInitialized) {
58+
(context?.applicationContext as WordPress).component().inject(this)
59+
}
60+
}
61+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package org.wordpress.android.reader.savedposts.resolver
2+
3+
import android.database.Cursor
4+
import org.wordpress.android.R
5+
import org.wordpress.android.datasets.wrappers.ReaderDatabaseWrapper
6+
import org.wordpress.android.datasets.wrappers.ReaderPostTableWrapper
7+
import org.wordpress.android.datasets.wrappers.ReaderTagTableWrapper
8+
import org.wordpress.android.models.ReaderPostList
9+
import org.wordpress.android.models.ReaderTag
10+
import org.wordpress.android.models.ReaderTagType.BOOKMARKED
11+
import org.wordpress.android.provider.query.QueryResult
12+
import org.wordpress.android.reader.savedposts.JetpackReaderSavedPostsFlag
13+
import org.wordpress.android.reader.savedposts.ReaderSavedPostsAnalyticsTracker
14+
import org.wordpress.android.reader.savedposts.ReaderSavedPostsAnalyticsTracker.ErrorType
15+
import org.wordpress.android.reader.savedposts.provider.ReaderSavedPostsProvider
16+
import org.wordpress.android.resolver.ContentResolverWrapper
17+
import org.wordpress.android.ui.prefs.AppPrefsWrapper
18+
import org.wordpress.android.util.publicdata.WordPressPublicData
19+
import org.wordpress.android.viewmodel.ContextProvider
20+
import javax.inject.Inject
21+
22+
class ReaderSavedPostsResolver @Inject constructor(
23+
private val jetpackReaderSavedPostsFlag: JetpackReaderSavedPostsFlag,
24+
private val queryResult: QueryResult,
25+
private val contextProvider: ContextProvider,
26+
private val contentResolverWrapper: ContentResolverWrapper,
27+
private val readerSavedPostsAnalyticsTracker: ReaderSavedPostsAnalyticsTracker,
28+
private val wordPressPublicData: WordPressPublicData,
29+
private val appPrefsWrapper: AppPrefsWrapper,
30+
private val readerPostTableWrapper: ReaderPostTableWrapper,
31+
private val readerTagTableWrapper: ReaderTagTableWrapper,
32+
private val readerDatabaseWrapper: ReaderDatabaseWrapper
33+
) {
34+
fun tryGetReaderSavedPosts(onSuccess: () -> Unit, onFailure: () -> Unit) {
35+
val isFeatureFlagEnabled = jetpackReaderSavedPostsFlag.isEnabled()
36+
if (!isFeatureFlagEnabled) {
37+
onFailure()
38+
return
39+
}
40+
val isFirstTry = appPrefsWrapper.getIsFirstTryReaderSavedPostsJetpack()
41+
if (!isFirstTry) {
42+
onFailure()
43+
return
44+
}
45+
46+
readerSavedPostsAnalyticsTracker.trackStart()
47+
appPrefsWrapper.saveIsFirstTryReaderSavedPostsJetpack(false)
48+
val savedPostsCursor = getReaderSavedPostsCursor()
49+
if (savedPostsCursor != null) {
50+
val posts = queryResult.getValue<ReaderPostList>(savedPostsCursor) ?: ReaderPostList()
51+
52+
updateReaderSavedPosts(onSuccess, onFailure, posts)
53+
} else {
54+
readerSavedPostsAnalyticsTracker.trackFailed(ErrorType.QuerySavedPostsError)
55+
onFailure()
56+
}
57+
}
58+
59+
private fun getReaderSavedPostsCursor(): Cursor? {
60+
val savedPostsUriValue =
61+
"content://${wordPressPublicData.currentPackageId()}.${ReaderSavedPostsProvider::class.simpleName}"
62+
return contentResolverWrapper.queryUri(
63+
contextProvider.getContext().contentResolver,
64+
savedPostsUriValue
65+
)
66+
}
67+
68+
@Suppress("TooGenericExceptionCaught", "SwallowedException")
69+
private fun updateReaderSavedPosts(
70+
onSuccess: () -> Unit,
71+
onFailure: () -> Unit,
72+
posts: ReaderPostList
73+
) {
74+
try {
75+
if (posts.isNotEmpty()) {
76+
readerDatabaseWrapper.reset(false)
77+
readerTagTableWrapper.addOrUpdateTag(
78+
ReaderTag(
79+
"",
80+
contextProvider.getContext().getString(R.string.reader_save_for_later_display_name),
81+
contextProvider.getContext().getString(R.string.reader_save_for_later_title),
82+
"",
83+
BOOKMARKED
84+
)
85+
)
86+
87+
requireNotNull(readerTagTableWrapper.getBookmarkTags()) {
88+
"unexpected null bookmark tags"
89+
}.first().let {
90+
readerPostTableWrapper.addOrUpdatePosts(it, posts)
91+
}
92+
}
93+
94+
readerSavedPostsAnalyticsTracker.trackSuccess(posts.size)
95+
onSuccess()
96+
} catch (exception: Exception) {
97+
readerSavedPostsAnalyticsTracker.trackFailed(ErrorType.GenericError(exception.message))
98+
onFailure()
99+
}
100+
}
101+
}

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

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import android.database.Cursor
55
import org.wordpress.android.fluxc.Dispatcher
66
import org.wordpress.android.fluxc.store.AccountStore
77
import org.wordpress.android.provider.query.QueryResult
8+
import org.wordpress.android.reader.savedposts.resolver.ReaderSavedPostsResolver
89
import org.wordpress.android.resolver.ContentResolverWrapper
910
import org.wordpress.android.sharedlogin.JetpackSharedLoginFlag
1011
import org.wordpress.android.sharedlogin.SharedLoginAnalyticsTracker
@@ -29,7 +30,8 @@ class SharedLoginResolver @Inject constructor(
2930
private val accountActionBuilderWrapper: AccountActionBuilderWrapper,
3031
private val appPrefsWrapper: AppPrefsWrapper,
3132
private val sharedLoginAnalyticsTracker: SharedLoginAnalyticsTracker,
32-
private val userFlagsResolver: UserFlagsResolver
33+
private val userFlagsResolver: UserFlagsResolver,
34+
private val readerSavedPostsResolver: ReaderSavedPostsResolver
3335
) {
3436
fun tryJetpackLogin() {
3537
val isFeatureFlagEnabled = jetpackSharedLoginFlag.isEnabled()
@@ -48,8 +50,22 @@ class SharedLoginResolver @Inject constructor(
4850
val accessToken = queryResult.getValue<String>(accessTokenCursor) ?: ""
4951
if (accessToken.isNotEmpty()) {
5052
sharedLoginAnalyticsTracker.trackLoginSuccess()
51-
dispatchUpdateAccessToken(accessToken)
52-
userFlagsResolver.tryGetUserFlags({ reloadMainScreen() }, { reloadMainScreen() })
53+
userFlagsResolver.tryGetUserFlags(
54+
{
55+
readerSavedPostsResolver.tryGetReaderSavedPosts(
56+
{
57+
dispatchUpdateAccessToken(accessToken)
58+
reloadMainScreen()
59+
},
60+
{
61+
reloadMainScreen()
62+
}
63+
)
64+
},
65+
{
66+
reloadMainScreen()
67+
}
68+
)
5369
} else {
5470
sharedLoginAnalyticsTracker.trackLoginFailed(ErrorType.WPNotLoggedInError)
5571
}

0 commit comments

Comments
 (0)