mirror of
https://github.com/samuelclay/NewsBlur.git
synced 2025-09-18 21:50:56 +00:00
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:
parent
09492328ce
commit
bfbaf20c79
5 changed files with 95 additions and 20 deletions
|
@ -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">
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue