1+ @file:OptIn(ExperimentalTime ::class )
2+
13package de.binarynoise.captiveportalautologin
24
5+ import java.io.FileNotFoundException
6+ import kotlin.time.Clock
7+ import kotlin.time.Duration.Companion.minutes
8+ import kotlin.time.Duration.Companion.seconds
9+ import kotlinx.coroutines.delay
310import android.content.Context
411import androidx.work.Constraints
512import androidx.work.CoroutineWorker
@@ -14,7 +21,10 @@ import de.binarynoise.captiveportalautologin.preferences.SharedPreferences
1421import de.binarynoise.captiveportalautologin.util.applicationContext
1522import de.binarynoise.filedb.JsonDB
1623import de.binarynoise.logger.Logger.log
24+ import de.binarynoise.util.okhttp.HttpStatusCodeException
1725import okhttp3.HttpUrl.Companion.toHttpUrl
26+ import kotlin.time.ExperimentalTime
27+ import kotlin.time.Instant
1828
1929const val API_BASE = " https://captiveportalautologin.binarynoise.de/api/"
2030
@@ -24,50 +34,83 @@ private val jsonDB = JsonDB(localCacheRoot)
2434// Worker class to handle the upload
2535class StatsWorker (appContext : Context , workerParams : WorkerParameters ) : CoroutineWorker(appContext, workerParams) {
2636
37+ var lastRetryTime by SharedPreferences .stats_last_retry_time
38+ private val retryCooldown = 5 .minutes
39+
40+ private fun hasRecentRetry (): Boolean {
41+ return (Clock .System .now() - Instant .fromEpochMilliseconds(lastRetryTime)) < retryCooldown
42+ }
43+
44+ private fun recordRetry () {
45+ lastRetryTime = Clock .System .now().toEpochMilliseconds()
46+ }
47+
2748 override suspend fun doWork (): Result {
2849 log(" StatsWorker started" )
29- val apiBaseFromPreference by SharedPreferences .api_base
30- val apiClient = ApiClient ((apiBaseFromPreference.takeUnless { it == " " } ? : API_BASE ).toHttpUrl())
3150
32- val harFiles = jsonDB.loadAll<HAR >(" har" )
33- harFiles.forEach { (key, har) ->
34- try {
35- apiClient.har.submitHar(key, har)
36- jsonDB.delete<HAR >(key, " har" )
37- log(" Uploaded HAR $key " )
38- } catch (e: Exception ) {
39- log(" Failed to upload har" , e)
40- // retry on the next worker run
41- return Result .retry()
42- }
51+ if (hasRecentRetry()) {
52+ log(" Recent retry detected, delaying this attempt to avoid spamming" )
53+ delay(retryCooldown)
4354 }
4455
45- val errorFiles = jsonDB.loadAll<Api .Liberator .Error >()
46- errorFiles.forEach { (key, error) ->
47- try {
48- apiClient.liberator.reportError(error)
49- jsonDB.delete<Api .Liberator .Error >(key)
50- log(" Uploaded Api.Liberator.Error $key " )
51- } catch (e: Exception ) {
52- log(" Failed to upload error" , e)
53- return Result .retry()
54- }
55- }
56+ val type = inputData.getString(" type" ) ? : return Result .failure()
57+ val key = inputData.getString(" key" ) ? : return Result .failure()
58+
59+ val apiBaseFromPreference by SharedPreferences .api_base
60+ val apiClient = ApiClient ((apiBaseFromPreference.takeUnless { it == " " } ? : API_BASE ).toHttpUrl())
5661
57- val successFiles = jsonDB.loadAll<Api .Liberator .Success >()
58- successFiles.forEach { (key, success) ->
59- try {
60- apiClient.liberator.reportSuccess(success)
61- jsonDB.delete<Api .Liberator .Success >(key)
62- log(" Uploaded Api.Liberator.Success $key " )
63- } catch (e: Exception ) {
64- log(" Failed to upload success" , e)
65- return Result .retry()
62+ try {
63+ when (type) {
64+ " har" -> {
65+ val har = jsonDB.load<HAR >(key, " har" )
66+ apiClient.har.submitHar(key, har)
67+ jsonDB.delete<HAR >(key, " har" )
68+ log(" Uploaded HAR $key " )
69+ }
70+ " error" -> {
71+ val error = jsonDB.load<Api .Liberator .Error >(key)
72+ apiClient.liberator.reportError(error)
73+ jsonDB.delete<Api .Liberator .Error >(key)
74+ log(" Uploaded Api.Liberator.Error $key " )
75+ }
76+ " success" -> {
77+ val success = jsonDB.load<Api .Liberator .Success >(key)
78+ apiClient.liberator.reportSuccess(success)
79+ jsonDB.delete<Api .Liberator .Success >(key)
80+ log(" Uploaded Api.Liberator.Success $key " )
81+ }
82+ }
83+
84+ return Result .success()
85+ } catch (e: FileNotFoundException ) {
86+ log(" Failed to upload $type $key " , e)
87+ return Result .failure()
88+ } catch (e: HttpStatusCodeException ) {
89+ when (e.code) {
90+ 429 -> {
91+ val timeout = e.response.header(" Retry-After" )?.toLongOrNull() ? : 0
92+ log(" Failed to upload $type $key : HTTP 429 - timeout: $timeout " )
93+ recordRetry()
94+ delay(timeout.seconds)
95+ return Result .retry()
96+ }
97+
98+ 500 , 501 , 502 , 503 , 504 , 507 -> {
99+ log(" Server error trying to upload $type $key : HTTP ${e.code} , trying again later" , e)
100+ recordRetry()
101+ return Result .retry()
102+ }
103+
104+ else -> {
105+ log(" Failed to upload $type $key : HTTP ${e.code} " , e)
106+ return Result .failure()
107+ }
66108 }
109+ } catch (e: Exception ) {
110+ log(" Failed to upload $type $key " , e)
111+ recordRetry()
112+ return Result .retry()
67113 }
68-
69- log(" StatsWorker finished" )
70- return Result .success()
71114 }
72115}
73116
@@ -80,7 +123,7 @@ object Stats : Api {
80123 override fun submitHar (name : String , har : HAR ) {
81124 val key = name
82125 jsonDB.store(key, har, " har" )
83- triggerUpload( )
126+ scheduleUpload( " har " , key )
84127 }
85128 }
86129
@@ -96,21 +139,23 @@ object Stats : Api {
96139 override fun reportError (error : Api .Liberator .Error ) {
97140 val key = " ${System .currentTimeMillis()} _${error.hashCode()} "
98141 jsonDB.store(key, error)
99- triggerUpload( )
142+ scheduleUpload( " error " , key )
100143 }
101144
102145 override fun reportSuccess (success : Api .Liberator .Success ) {
103146 val key = " ${System .currentTimeMillis()} _${success.hashCode()} "
104147 jsonDB.store(key, success)
105- triggerUpload( )
148+ scheduleUpload( " success " , key )
106149 }
107150 }
108151
109- // Schedule the WorkRequest
110- fun triggerUpload () {
111- val constraints = Constraints .Builder ().setRequiredNetworkType(NetworkType .CONNECTED ).build()
112- val uploadRequest = OneTimeWorkRequestBuilder <StatsWorker >().setConstraints(constraints).build()
152+ private fun scheduleUpload (type : String , key : String ) {
153+ val constraints = Constraints .Builder ().setRequiredNetworkType(NetworkType .UNMETERED ).build()
154+ val inputData = androidx.work.workDataOf(" type" to type, " key" to key)
155+ val uploadRequest =
156+ OneTimeWorkRequestBuilder <StatsWorker >().setConstraints(constraints).setInputData(inputData).build()
157+
113158 WorkManager .getInstance(applicationContext).enqueue(uploadRequest)
114- log(" Scheduled upload" )
159+ log(" Scheduled upload for $type : $key " )
115160 }
116161}
0 commit comments