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;