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" /> android:permission="android.permission.BIND_JOB_SERVICE" />
<service android:name=".widget.WidgetRemoteViewsService" <service android:name=".widget.WidgetRemoteViewsService"
android:permission="android.permission.BIND_REMOTEVIEWS" /> 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" <receiver android:name=".service.BootReceiver"
android:exported="true"> android:exported="true">

View file

@ -1,15 +1,20 @@
package com.newsblur package com.newsblur
import android.app.Application import android.app.Application
import android.app.job.JobScheduler
import android.content.Context
import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner import androidx.lifecycle.ProcessLifecycleOwner
import com.newsblur.service.SubscriptionSyncService
import com.newsblur.util.Log
class NbApplication : Application(), DefaultLifecycleObserver { class NbApplication : Application(), DefaultLifecycleObserver {
override fun onCreate() { override fun onCreate() {
super<Application>.onCreate() super<Application>.onCreate()
ProcessLifecycleOwner.get().lifecycle.addObserver(this) ProcessLifecycleOwner.get().lifecycle.addObserver(this)
scheduleSubscriptionSync()
} }
override fun onStart(owner: LifecycleOwner) { override fun onStart(owner: LifecycleOwner) {
@ -22,6 +27,15 @@ class NbApplication : Application(), DefaultLifecycleObserver {
isAppForeground = false 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 { companion object {
@JvmStatic @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.CoroutineScope
import kotlinx.coroutines.Deferred import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.text.DateFormat import java.text.DateFormat
@ -57,7 +58,7 @@ interface SubscriptionManager {
/** /**
* Sync subscription state between NewsBlur and Play Store. * Sync subscription state between NewsBlur and Play Store.
*/ */
fun syncActiveSubscription() fun syncActiveSubscription(): Job
/** /**
* Notify backend of active Play Store subscription. * Notify backend of active Play Store subscription.
@ -159,7 +160,7 @@ class SubscriptionManagerImpl(
} }
override fun syncSubscriptionState() { override fun syncSubscriptionState() {
scope.launch { scope.launch(Dispatchers.Default) {
if (hasActiveSubscription()) syncActiveSubscription() if (hasActiveSubscription()) syncActiveSubscription()
else syncAvailableSubscription() else syncAvailableSubscription()
} }
@ -173,21 +174,21 @@ class SubscriptionManagerImpl(
billingClient.launchBillingFlow(activity, billingFlowParams) billingClient.launchBillingFlow(activity, billingFlowParams)
} }
override fun syncActiveSubscription() { override fun syncActiveSubscription() = scope.launch(Dispatchers.Default) {
scope.launch(Dispatchers.Default) { val hasNewsBlurSubscription = PrefsUtils.getIsPremium(context)
val hasNewsBlurSubscription = PrefsUtils.getIsPremium(context) val activePlayStoreSubscription = getActiveSubscriptionAsync().await()
val activePlayStoreSubscription = getActiveSubscriptionAsync().await()
if (hasNewsBlurSubscription || activePlayStoreSubscription != null) { if (hasNewsBlurSubscription || activePlayStoreSubscription != null) {
listener?.let {
val renewalString: String? = getRenewalMessage(activePlayStoreSubscription) val renewalString: String? = getRenewalMessage(activePlayStoreSubscription)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
listener?.onActiveSubscription(renewalString) it.onActiveSubscription(renewalString)
} }
} }
}
if (!hasNewsBlurSubscription && activePlayStoreSubscription != null) { if (!hasNewsBlurSubscription && activePlayStoreSubscription != null) {
saveReceipt(activePlayStoreSubscription) saveReceipt(activePlayStoreSubscription)
}
} }
} }
@ -210,15 +211,13 @@ class SubscriptionManagerImpl(
) )
} }
private suspend fun syncAvailableSubscription() { private suspend fun syncAvailableSubscription() = scope.launch(Dispatchers.Default) {
scope.launch(Dispatchers.Default) { val skuDetails = getAvailableSubscriptionAsync().await()
val skuDetails = getAvailableSubscriptionAsync().await() withContext(Dispatchers.Main) {
withContext(Dispatchers.Main) { skuDetails?.let {
skuDetails?.let { Log.d(this, it.toString())
Log.d(this, it.toString()) listener?.onAvailableSubscription(it)
listener?.onAvailableSubscription(it) } ?: listener?.onBillingConnectionError()
} ?: 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. // 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; 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 // how many total attemtps to make at a single API call
public static final int MAX_API_TRIES = 3; public static final int MAX_API_TRIES = 3;