diff --git a/clients/android/NewsBlur/AndroidManifest.xml b/clients/android/NewsBlur/AndroidManifest.xml index 2d74079da..4fa6da982 100644 --- a/clients/android/NewsBlur/AndroidManifest.xml +++ b/clients/android/NewsBlur/AndroidManifest.xml @@ -147,6 +147,10 @@ android:permission="android.permission.BIND_JOB_SERVICE" /> + diff --git a/clients/android/NewsBlur/src/com/newsblur/NbApplication.kt b/clients/android/NewsBlur/src/com/newsblur/NbApplication.kt index 32b2253b0..00f79965c 100644 --- a/clients/android/NewsBlur/src/com/newsblur/NbApplication.kt +++ b/clients/android/NewsBlur/src/com/newsblur/NbApplication.kt @@ -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.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 diff --git a/clients/android/NewsBlur/src/com/newsblur/service/SubscriptionSyncService.kt b/clients/android/NewsBlur/src/com/newsblur/service/SubscriptionSyncService.kt new file mode 100644 index 000000000..0a1bc3a5c --- /dev/null +++ b/clients/android/NewsBlur/src/com/newsblur/service/SubscriptionSyncService.kt @@ -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() + } +} \ No newline at end of file diff --git a/clients/android/NewsBlur/src/com/newsblur/subscription/SubscriptionManager.kt b/clients/android/NewsBlur/src/com/newsblur/subscription/SubscriptionManager.kt index a108e730b..ffcad59d6 100644 --- a/clients/android/NewsBlur/src/com/newsblur/subscription/SubscriptionManager.kt +++ b/clients/android/NewsBlur/src/com/newsblur/subscription/SubscriptionManager.kt @@ -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() } } diff --git a/clients/android/NewsBlur/src/com/newsblur/util/AppConstants.java b/clients/android/NewsBlur/src/com/newsblur/util/AppConstants.java index 987d0380e..a4001bfc3 100644 --- a/clients/android/NewsBlur/src/com/newsblur/util/AppConstants.java +++ b/clients/android/NewsBlur/src/com/newsblur/util/AppConstants.java @@ -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;