Skip to content

Instantly share code, notes, and snippets.

@LouisCAD
Last active January 9, 2018 06:11
Show Gist options
  • Save LouisCAD/ea7228360250b34d8da4668768018839 to your computer and use it in GitHub Desktop.
Save LouisCAD/ea7228360250b34d8da4668768018839 to your computer and use it in GitHub Desktop.
A fix for Firebase Job Dispatcher issue #6 (https://github.com/firebase/firebase-jobdispatcher-android/issues/6). You need to extend PersistedJobService instead of JobService for this to work, and call Jobs.schedulePresisted(tag)
import com.google.android.gms.gcm.GcmTaskService
import com.google.android.gms.gcm.TaskParams
import com.example.androidapp.jobs.Jobs
import com.example.androidapp.jobs.tagsOfScheduledJobs
import timber.log.Timber
/**
* See [this issue](https://github.com/firebase/firebase-jobdispatcher-android/issues/6).
*/
class JobDispatcherReschedulerWorkaroundService : GcmTaskService() {
override fun onRunTask(taskParams: TaskParams?) = 0
override fun onInitializeTasks() {
Timber.i("onInitializeTasks")
tagsOfScheduledJobs.forEach { tag ->
Jobs.schedulePersisted(tag)
}
}
}
import android.content.Context
import com.firebase.jobdispatcher.*
import com.firebase.jobdispatcher.Constraint.*
import com.firebase.jobdispatcher.RetryStrategy.DEFAULT_EXPONENTIAL
import com.example.androidapp.illegal
import com.example.androidapp.extensions.setService
import com.example.androidapp.extensions.shouldReplaceCurrent
import com.example.androidapp.prefs.DefaultPrefs
import splitties.init.appCtx
import timber.log.Timber
object Jobs {
private val jobDispatcher = FirebaseJobDispatcher(GooglePlayDriver(appCtx))
object Tags {
const val EXAMPLE_JOB_1 = "example_job1"
const val EXAMPLE_JOB_2 = "example_job2"
}
fun schedulePersisted(jobTag: String) = setupNewJob {
apply {
// Common to all the persisted jobs.
tag = jobTag
lifetime = Lifetime.FOREVER
shouldReplaceCurrent = true
trigger = Trigger.NOW
retryStrategy = DEFAULT_EXPONENTIAL
}
when (jobTag) {
Tags.EXAMPLE_JOB_1 -> {
setService<Example1JobService>()
setConstraints(ON_ANY_NETWORK)
}
Tags.EXAMPLE_JOB_2 -> {
setService<Example2JobService>()
val networkConstraint = if (NetworkPrefs.uploadOnWifiOnly) ON_UNMETERED_NETWORK else ON_ANY_NETWORK
if (NetworkPrefs.uploadOnlyWhenCharging) {
setConstraints(DEVICE_CHARGING, networkConstraint)
} else setConstraints(networkConstraint)
}
else -> illegal("Unknown job tag: $jobTag")
}
persistJobScheduledState(jobTag)
}.schedule()
fun cancel(jobTag: String) {
persistJobCancelled(jobTag)
val result = jobDispatcher.cancel(jobTag)
// region Error cases handling (throws in debug, reports in release)
if (result == FirebaseJobDispatcher.CANCEL_RESULT_SUCCESS) return Timber.d("Successfully cancelled job with tag: $jobTag")
val failure = when (result) {
FirebaseJobDispatcher.CANCEL_RESULT_NO_DRIVER_AVAILABLE -> "NO_DRIVER_AVAILABLE"
FirebaseJobDispatcher.CANCEL_RESULT_UNKNOWN_ERROR -> "UNKNOWN_ERROR"
else -> illegal("Unknown result: $result")
}
val e = RuntimeException("Job with tag $jobTag scheduling failed: $failure")
if (BuildConfig.DEBUG) throw e else Timber.wtf(e)
// endregion
}
private inline fun setupNewJob(config: Job.Builder.() -> Unit): Job {
return jobDispatcher.newJobBuilder().apply { config() }.build()
}
@Suppress("NOTHING_TO_INLINE")
private inline fun Job.schedule() {
val result = jobDispatcher.schedule(this)
// region Error cases handling (throws in debug, reports in release)
if (result == FirebaseJobDispatcher.SCHEDULE_RESULT_SUCCESS) return Timber.d("Successfully scheduled job with tag: $tag")
val failure = when (result) {
FirebaseJobDispatcher.SCHEDULE_RESULT_UNSUPPORTED_TRIGGER -> "UNSUPPORTED_TRIGGER"
FirebaseJobDispatcher.SCHEDULE_RESULT_BAD_SERVICE -> "BAD_SERVICE"
FirebaseJobDispatcher.SCHEDULE_RESULT_NO_DRIVER_AVAILABLE -> "NO_DRIVER_AVAILABLE"
FirebaseJobDispatcher.SCHEDULE_RESULT_UNKNOWN_ERROR -> "UNKNOWN_ERROR"
else -> illegal("Unknown result: $result")
}
val e = RuntimeException("Job with tag $tag scheduling failed: $failure")
if (BuildConfig.DEBUG) throw e else Timber.wtf(e)
// endregion
}
}
abstract class PersistedJobService : JobService() {
/** Call this method instead of `jobFinished(jobs, false)` */
fun jobDone(job: JobParameters) {
if (stateOf(job.tag) == STATE_RUNNING) setState(job.tag, STATE_NOT_SCHEDULED)
jobFinished(job, false)
}
/** Call this method instead of `jobFinished(jobs, true)` */
fun jobFailed(job: JobParameters) {
if (stateOf(job.tag) == STATE_RUNNING) setState(job.tag, STATE_SCHEDULED)
jobFinished(job, true)
}
abstract fun startJob(job: JobParameters): Boolean
abstract fun stopJob(job: JobParameters): Boolean
override final fun onStartJob(job: JobParameters): Boolean {
setState(job.tag, STATE_RUNNING)
return startJob(job)
}
override final fun onStopJob(job: JobParameters): Boolean {
val shouldBeRetried = stopJob(job)
if (shouldBeRetried || job.isRecurring) setState(job.tag, STATE_SCHEDULED)
else if (stateOf(job.tag) == STATE_RUNNING) setState(job.tag, STATE_NOT_SCHEDULED)
return shouldBeRetried
}
}
private fun persistJobScheduledState(jobTag: String) = setState(jobTag, STATE_SCHEDULED)
private fun persistJobCancelled(jobTag: String) = setState(jobTag, STATE_NOT_SCHEDULED)
private fun stateOf(jobTag: String) = jobsStates.getInt(jobTag, STATE_NOT_SCHEDULED)
private fun setState(jobTag: String, newState: Int) {
jobsStates.edit().putInt(jobTag, newState).apply()
}
/** Used to reschedule them */
val tagsOfScheduledJobs: Set<String>
get() = jobsStates.all.filterNot { (_, state) ->
(state as Int) == STATE_NOT_SCHEDULED
}.keys
private val jobsStates = appCtx.getSharedPreferences("jobs_states", Context.MODE_PRIVATE)!!
private const val STATE_NOT_SCHEDULED = 0
private const val STATE_SCHEDULED = 1
private const val STATE_RUNNING = 2
fun illegal(errorMessage: String? = null): Nothing = throw IllegalStateException(errorMessage)
import splitties.preferences.DefaultPreferences
/** Property SharedPreferences. See [Splitties](https://github.com/LouisCAD/Splitties#preferences)
object NetworkPrefs : DefaultPreferences() {
var uploadOnWifiOnly by BoolPref("upload_on_unmetered_networks", true)
var uploadOnlyWhenCharging by BoolPref("upload_only_when_charging", false)
}
<manifest>
...
<application/>
...
<!-- region Firebase Job Dispatcher reschedule workaround -->
<service
android:name=".JobDispatcherReschedulerWorkaroundService"
android:permission="com.google.android.gms.permission.BIND_NETWORK_TASK_SERVICE">
<intent-filter>
<action android:name="com.google.android.gms.gcm.ACTION_TASK_READY"/>
</intent-filter>
</service>
<!-- endregion -->
...
</application>
</manifest>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment