Skip to content

Commit

Permalink
Store encoded request to disk when using WorkManager
Browse files Browse the repository at this point in the history
  • Loading branch information
tillh-stripe committed May 16, 2024
1 parent 6ecb6b5 commit 6a4e414
Show file tree
Hide file tree
Showing 7 changed files with 125 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ import com.stripe.android.core.injection.STRIPE_ACCOUNT_ID
import com.stripe.android.core.networking.AnalyticsRequestExecutor
import com.stripe.android.core.networking.AnalyticsRequestFactory
import com.stripe.android.core.networking.AnalyticsRequestV2Executor
import com.stripe.android.core.networking.AnalyticsRequestV2Storage
import com.stripe.android.core.networking.ApiRequest
import com.stripe.android.core.networking.DefaultAnalyticsRequestExecutor
import com.stripe.android.core.networking.DefaultAnalyticsRequestV2Executor
import com.stripe.android.core.networking.DefaultStripeNetworkClient
import com.stripe.android.core.networking.NetworkTypeDetector
import com.stripe.android.core.networking.RealAnalyticsRequestV2Storage
import com.stripe.android.core.networking.StripeNetworkClient
import com.stripe.android.core.utils.ContextUtils.packageInfo
import com.stripe.android.core.utils.IsWorkManagerAvailable
Expand Down Expand Up @@ -56,6 +58,10 @@ import kotlin.coroutines.CoroutineContext
)
internal interface FinancialConnectionsSheetSharedModule {

@Binds
@Singleton
fun bindsAnalyticsRequestV2Storage(impl: RealAnalyticsRequestV2Storage): AnalyticsRequestV2Storage

@Binds
@Singleton
fun bindsAnalyticsRequestV2Executor(impl: DefaultAnalyticsRequestV2Executor): AnalyticsRequestV2Executor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class DefaultAnalyticsRequestV2Executor @Inject constructor(
private val application: Application,
private val networkClient: StripeNetworkClient,
private val logger: Logger,
private val storage: AnalyticsRequestV2Storage,
private val isWorkManagerAvailable: IsWorkManagerAvailable,
) : AnalyticsRequestV2Executor {

Expand All @@ -33,7 +34,8 @@ class DefaultAnalyticsRequestV2Executor @Inject constructor(

private suspend fun enqueueRequest(request: AnalyticsRequestV2): Boolean {
val workManager = WorkManager.getInstance(application)
val inputData = SendAnalyticsRequestV2Worker.createInputData(request)
val id = storage.store(request)
val inputData = SendAnalyticsRequestV2Worker.createInputData(id)

val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.stripe.android.core.networking

import android.app.Application
import android.content.Context
import android.content.SharedPreferences
import androidx.annotation.RestrictTo
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.util.UUID
import javax.inject.Inject

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
interface AnalyticsRequestV2Storage {
suspend fun store(request: AnalyticsRequestV2): String
suspend fun retrieve(id: String): AnalyticsRequestV2?
suspend fun delete(id: String)
}

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
class RealAnalyticsRequestV2Storage private constructor(
private val sharedPrefs: SharedPreferences,
private val dispatcher: CoroutineDispatcher = Dispatchers.IO,
) : AnalyticsRequestV2Storage {

@Inject constructor(application: Application) : this(
sharedPrefs = application.getSharedPreferences(
"StripeAnalyticsRequestV2Storage",
Context.MODE_PRIVATE
),
)

override suspend fun store(request: AnalyticsRequestV2): String = withContext(dispatcher) {
val id = UUID.randomUUID().toString()
val encodedRequest = Json.encodeToString(request)
sharedPrefs
.edit()
.putString(id, encodedRequest)
.apply()
id
}

override suspend fun retrieve(id: String): AnalyticsRequestV2? = withContext(dispatcher) {
val encodedRequest = sharedPrefs.getString(id, null) ?: return@withContext null

sharedPrefs.edit().remove(id).apply()

runCatching<AnalyticsRequestV2> {
Json.decodeFromString(encodedRequest)
}.getOrNull()
}

override suspend fun delete(id: String) = withContext(dispatcher) {
sharedPrefs.edit().remove(id).apply()
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
package com.stripe.android.core.networking

import android.app.Application
import android.content.Context
import androidx.annotation.VisibleForTesting
import androidx.work.CoroutineWorker
import androidx.work.Data
import androidx.work.WorkerParameters
import androidx.work.workDataOf
import com.stripe.android.core.exception.InvalidRequestException
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json

private const val DataKey = "data"
private const val MaxAttempts = 5
Expand All @@ -29,43 +28,49 @@ internal class SendAnalyticsRequestV2Worker(
if (error.shouldRetry && runAttemptCount < MaxAttempts) {
Result.retry()
} else {
deleteRequest()
Result.failure()
}
},
)
}

private inline fun withRequest(block: (AnalyticsRequestV2) -> Result): Result {
val request = getRequest(inputData) ?: return Result.failure()
private suspend inline fun withRequest(block: (AnalyticsRequestV2) -> Result): Result {
val id = inputData.getString(DataKey) ?: return Result.failure()
val request = storage(applicationContext).retrieve(id) ?: return Result.failure()
val workManagerRequest = request.withWorkManagerParams(runAttemptCount)
return block(workManagerRequest)
}

private suspend fun deleteRequest() {
val id = inputData.getString(DataKey) ?: return
storage(applicationContext).delete(id)
}

companion object {

const val TAG = "SendAnalyticsRequestV2Worker"

var networkClient: StripeNetworkClient = DefaultStripeNetworkClient()
private set

fun createInputData(request: AnalyticsRequestV2): Data {
val encodedRequest = Json.encodeToString(request)
return workDataOf(DataKey to encodedRequest)
}
var storage: (Context) -> AnalyticsRequestV2Storage =
{ RealAnalyticsRequestV2Storage(it.applicationContext as Application) }
private set

private fun getRequest(data: Data): AnalyticsRequestV2? {
val encodedRequest = data.getString(DataKey)
return encodedRequest?.let {
runCatching<AnalyticsRequestV2> {
Json.decodeFromString(it)
}.getOrNull()
}
fun createInputData(id: String): Data {
return workDataOf(DataKey to id)
}

@VisibleForTesting
fun setNetworkClient(networkClient: StripeNetworkClient) {
this.networkClient = networkClient
}

@VisibleForTesting
fun setStorage(storage: AnalyticsRequestV2Storage) {
this.storage = { storage }
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import androidx.work.WorkManager
import androidx.work.testing.WorkManagerTestInitHelper
import com.google.common.truth.Truth.assertThat
import com.stripe.android.core.Logger
import com.stripe.android.core.utils.FakeAnalyticsRequestV2Storage
import com.stripe.android.core.utils.FakeStripeNetworkClient
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.test.runTest
Expand All @@ -29,11 +30,13 @@ internal class DefaultAnalyticsRequestV2ExecutorTest {
@Test
fun `Enqueues requests directly if WorkManager is available`() = runTest {
val networkClient = FakeStripeNetworkClient()
val storage = FakeAnalyticsRequestV2Storage()

val executor = DefaultAnalyticsRequestV2Executor(
application = application,
networkClient = networkClient,
logger = Logger.noop(),
storage = storage,
isWorkManagerAvailable = { true },
)

Expand All @@ -49,11 +52,13 @@ internal class DefaultAnalyticsRequestV2ExecutorTest {
@Test
fun `Executes requests directly if WorkManager isn't available`() = runTest {
val networkClient = FakeStripeNetworkClient()
val storage = FakeAnalyticsRequestV2Storage()

val executor = DefaultAnalyticsRequestV2Executor(
application = application,
networkClient = networkClient,
logger = Logger.noop(),
storage = storage,
isWorkManagerAvailable = { false },
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import androidx.work.testing.TestListenableWorkerBuilder
import com.google.common.truth.Truth.assertThat
import com.stripe.android.core.exception.APIConnectionException
import com.stripe.android.core.exception.InvalidRequestException
import com.stripe.android.core.utils.FakeAnalyticsRequestV2Storage
import com.stripe.android.core.utils.FakeStripeNetworkClient
import kotlinx.coroutines.test.runTest
import org.junit.Test
Expand Down Expand Up @@ -46,17 +47,20 @@ internal class SendAnalyticsRequestV2WorkerTest {
executeRequest: () -> StripeResponse<String>,
expectedResult: ListenableWorker.Result,
) {
val networkClient = FakeStripeNetworkClient(executeRequest = executeRequest)
SendAnalyticsRequestV2Worker.setNetworkClient(networkClient)

val storage = FakeAnalyticsRequestV2Storage()
SendAnalyticsRequestV2Worker.setStorage(storage)

val request = mockAnalyticsRequest()
val input = SendAnalyticsRequestV2Worker.createInputData(request)
val id = storage.store(request)
val input = SendAnalyticsRequestV2Worker.createInputData(id)

val worker = TestListenableWorkerBuilder<SendAnalyticsRequestV2Worker>(application)
.setInputData(input)
.build()

val networkClient = FakeStripeNetworkClient(executeRequest = executeRequest)

SendAnalyticsRequestV2Worker.setNetworkClient(networkClient)

val result = worker.doWork()
assertThat(result).isEqualTo(expectedResult)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.stripe.android.core.utils

import com.stripe.android.core.networking.AnalyticsRequestV2
import com.stripe.android.core.networking.AnalyticsRequestV2Storage
import java.util.UUID

internal class FakeAnalyticsRequestV2Storage : AnalyticsRequestV2Storage {

private val store = mutableMapOf<String, AnalyticsRequestV2>()

override suspend fun store(request: AnalyticsRequestV2): String {
val id = UUID.randomUUID().toString()
store[id] = request
return id
}

override suspend fun retrieve(id: String): AnalyticsRequestV2? {
return store[id]
}

override suspend fun delete(id: String) {
store.remove(id)
}
}

0 comments on commit 6a4e414

Please sign in to comment.