Subscription sync service running every 24 hours to sync if necessary the user's premium subscription state between NewsBlur and Play Store.

This commit is contained in:
sictiru 2021-12-28 16:51:17 -08:00
parent 09492328ce
commit bfbaf20c79
5 changed files with 95 additions and 20 deletions

View file

@ -147,6 +147,10 @@
android:permission="android.permission.BIND_JOB_SERVICE" />
<service android:name=".widget.WidgetRemoteViewsService"
android:permission="android.permission.BIND_REMOTEVIEWS" />
<service
android:name=".service.SubscriptionSyncService"
android:permission="android.permission.BIND_JOB_SERVICE"
android:exported="false" />
<receiver android:name=".service.BootReceiver"
android:exported="true">

View file

@ -1,15 +1,20 @@
package com.newsblur
import android.app.Application
import android.app.job.JobScheduler
import android.content.Context
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
import com.newsblur.service.SubscriptionSyncService
import com.newsblur.util.Log
class NbApplication : Application(), DefaultLifecycleObserver {
override fun onCreate() {
super<Application>.onCreate()
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
scheduleSubscriptionSync()
}
override fun onStart(owner: LifecycleOwner) {
@ -22,6 +27,15 @@ class NbApplication : Application(), DefaultLifecycleObserver {
isAppForeground = false
}
private fun scheduleSubscriptionSync() {
val jobScheduler = getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
val scheduledSubscriptionJob = jobScheduler.allPendingJobs.find { it.id == SubscriptionSyncService.JOB_ID }
if (scheduledSubscriptionJob == null) {
val result: Int = jobScheduler.schedule(SubscriptionSyncService.createJobInfo(this))
Log.d(this, "Scheduled subscription result: ${if (result == JobScheduler.RESULT_FAILURE) "failed" else "completed"}")
}
}
companion object {
@JvmStatic

View file

@ -0,0 +1,55 @@
package com.newsblur.service
import android.app.job.JobInfo
import android.app.job.JobParameters
import android.app.job.JobService
import android.content.ComponentName
import android.content.Context
import com.newsblur.subscription.SubscriptionManagerImpl
import com.newsblur.util.AppConstants
import com.newsblur.util.Log
import com.newsblur.util.NBScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
/**
* Service to sync user subscription with NewsBlur backend.
*
* Mostly interested in handling the state where there is an active
* subscription in Play Store but NewsBlur doesn't know about it.
* This could occur when the user has renewed the subscription
* via Play Store.
*/
class SubscriptionSyncService : JobService() {
override fun onStartJob(params: JobParameters?): Boolean {
Log.d(this, "onStartJob")
NBScope.launch(Dispatchers.Default) {
val subscriptionManager = SubscriptionManagerImpl(this@SubscriptionSyncService, this)
val job = subscriptionManager.syncActiveSubscription()
job.invokeOnCompletion {
Log.d(this, "sync active subscription completed.")
// manually trigger jobFinished after work is done
jobFinished(params, false)
}
}
return true // returning true due to background thread work
}
override fun onStopJob(params: JobParameters?): Boolean = false
companion object {
const val JOB_ID = 2021
fun createJobInfo(context: Context): JobInfo = JobInfo.Builder(JOB_ID,
ComponentName(context, SubscriptionSyncService::class.java))
.apply {
// sync every 24 hours
setPeriodic(AppConstants.BG_SUBSCRIPTION_SYNC_CYCLE_MILLIS)
setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
setBackoffCriteria(JobInfo.DEFAULT_INITIAL_BACKOFF_MILLIS, JobInfo.BACKOFF_POLICY_EXPONENTIAL)
setPersisted(true)
}.build()
}
}

View file

@ -25,6 +25,7 @@ import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.text.DateFormat
@ -57,7 +58,7 @@ interface SubscriptionManager {
/**
* Sync subscription state between NewsBlur and Play Store.
*/
fun syncActiveSubscription()
fun syncActiveSubscription(): Job
/**
* Notify backend of active Play Store subscription.
@ -159,7 +160,7 @@ class SubscriptionManagerImpl(
}
override fun syncSubscriptionState() {
scope.launch {
scope.launch(Dispatchers.Default) {
if (hasActiveSubscription()) syncActiveSubscription()
else syncAvailableSubscription()
}
@ -173,21 +174,21 @@ class SubscriptionManagerImpl(
billingClient.launchBillingFlow(activity, billingFlowParams)
}
override fun syncActiveSubscription() {
scope.launch(Dispatchers.Default) {
val hasNewsBlurSubscription = PrefsUtils.getIsPremium(context)
val activePlayStoreSubscription = getActiveSubscriptionAsync().await()
override fun syncActiveSubscription() = scope.launch(Dispatchers.Default) {
val hasNewsBlurSubscription = PrefsUtils.getIsPremium(context)
val activePlayStoreSubscription = getActiveSubscriptionAsync().await()
if (hasNewsBlurSubscription || activePlayStoreSubscription != null) {
if (hasNewsBlurSubscription || activePlayStoreSubscription != null) {
listener?.let {
val renewalString: String? = getRenewalMessage(activePlayStoreSubscription)
withContext(Dispatchers.Main) {
listener?.onActiveSubscription(renewalString)
it.onActiveSubscription(renewalString)
}
}
}
if (!hasNewsBlurSubscription && activePlayStoreSubscription != null) {
saveReceipt(activePlayStoreSubscription)
}
if (!hasNewsBlurSubscription && activePlayStoreSubscription != null) {
saveReceipt(activePlayStoreSubscription)
}
}
@ -210,15 +211,13 @@ class SubscriptionManagerImpl(
)
}
private suspend fun syncAvailableSubscription() {
scope.launch(Dispatchers.Default) {
val skuDetails = getAvailableSubscriptionAsync().await()
withContext(Dispatchers.Main) {
skuDetails?.let {
Log.d(this, it.toString())
listener?.onAvailableSubscription(it)
} ?: listener?.onBillingConnectionError()
}
private suspend fun syncAvailableSubscription() = scope.launch(Dispatchers.Default) {
val skuDetails = getAvailableSubscriptionAsync().await()
withContext(Dispatchers.Main) {
skuDetails?.let {
Log.d(this, it.toString())
listener?.onAvailableSubscription(it)
} ?: listener?.onBillingConnectionError()
}
}

View file

@ -41,6 +41,9 @@ public class AppConstants {
// to account for the fact that it is approximate, and missing a cycle is bad.
public static final long BG_SERVICE_CYCLE_MILLIS = AUTO_SYNC_TIME_MILLIS + 30L * 1000L;
// how often to trigger the job scheduler to sync subscription state.
public static final long BG_SUBSCRIPTION_SYNC_CYCLE_MILLIS = 24L * 60 * 60 * 1000L;
// how many total attemtps to make at a single API call
public static final int MAX_API_TRIES = 3;