Repository: bitwarden/android
Pull request #4968: [PM-19820] Replace `ResultCallAdapterFactory` in authenticator module
Author: login: SaintPatrck
Created: 2025-04-02T18:21:21Z | Merged: 2025-04-02T18:55:58Z

## 🎟️ Tracking

PM-19820

## 📔 Objective

The `ResultCall`, `ResultCallAdapter`, `ResultCallAdapterFactory`, and `ResultCallAdapterTest` files were removed and `NetworkResultCallAdapterFactory` was added. Updated references to the factory. Also, updates the return type in the ConfigApi and ConfigServiceImpl from `Result` to `NetworkResult`. Adds a dependency to the `network` module.

## ⏰ Reminders before review

- Contributor guidelines followed
- All formatters and local linters executed and passed
- Written new unit and / or integration tests where applicable
- Protected functional changes with optionality (feature flags)
- Used internationalization (i18n) for all UI strings
- CI builds passed
- Communicated to DevOps any deployment requirements
- Updated any necessary documentation (Confluence, contributing docs) or informed the documentation team

## 🦮 Reviewer guidelines

<!-- Suggested interactions but feel free to use (or not) as you desire! -->

- 👍 (`:+1:`) or similar for great changes
- 📝 (`:memo:`) or ℹ️ (`:information_source:`) for notes or general info
- ❓ (`:question:`) for questions
- 🤔 (`:thinking:`) or 💭 (`:thought_balloon:`) for more open inquiry that's not quite a confirmed issue and could potentially benefit from discussion
- 🎨 (`:art:`) for suggestions / improvements
- ❌ (`:x:`) or ⚠️ (`:warning:`) for more significant problems or concerns needing attention
- 🌱 (`:seedling:`) or ♻️ (`:recycle:`) for future improvements or indications of technical debt
- ⛏ (`:pick:`) for minor or nitpick changes



Diff:
diff --git a/authenticator/build.gradle.kts b/authenticator/build.gradle.kts
index a2bd66fe72d..572f831a609 100644
--- a/authenticator/build.gradle.kts
+++ b/authenticator/build.gradle.kts
@@ -152,6 +152,7 @@ dependencies {
     implementation(files("libs/authenticatorbridge-1.0.0-release.aar"))
 
     implementation(project(":core"))
+    implementation(project(":network"))
 
     implementation(libs.androidx.activity.compose)
     implementation(libs.androidx.appcompat)
diff --git a/authenticator/src/main/kotlin/com/bitwarden/authenticator/data/platform/datasource/network/api/ConfigApi.kt b/authenticator/src/main/kotlin/com/bitwarden/authenticator/data/platform/datasource/network/api/ConfigApi.kt
index f3e3d2aed26..2a0bcec95bb 100644
--- a/authenticator/src/main/kotlin/com/bitwarden/authenticator/data/platform/datasource/network/api/ConfigApi.kt
+++ b/authenticator/src/main/kotlin/com/bitwarden/authenticator/data/platform/datasource/network/api/ConfigApi.kt
@@ -1,6 +1,7 @@
 package com.bitwarden.authenticator.data.platform.datasource.network.api
 
 import com.bitwarden.authenticator.data.platform.datasource.network.model.ConfigResponseJson
+import com.bitwarden.network.model.NetworkResult
 import retrofit2.http.GET
 
 /**
@@ -9,5 +10,5 @@ import retrofit2.http.GET
 interface ConfigApi {
 
     @GET("config")
-    suspend fun getConfig(): Result<ConfigResponseJson>
+    suspend fun getConfig(): NetworkResult<ConfigResponseJson>
 }
diff --git a/authenticator/src/main/kotlin/com/bitwarden/authenticator/data/platform/datasource/network/core/ResultCall.kt b/authenticator/src/main/kotlin/com/bitwarden/authenticator/data/platform/datasource/network/core/ResultCall.kt
deleted file mode 100644
index 979b11ab21b..00000000000
--- a/authenticator/src/main/kotlin/com/bitwarden/authenticator/data/platform/datasource/network/core/ResultCall.kt
+++ /dev/null
@@ -1,98 +0,0 @@
-@file:OmitFromCoverage
-
-package com.bitwarden.authenticator.data.platform.datasource.network.core
-
-import com.bitwarden.core.annotation.OmitFromCoverage
-import com.bitwarden.core.data.util.asFailure
-import com.bitwarden.core.data.util.asSuccess
-import okhttp3.Request
-import okio.IOException
-import okio.Timeout
-import retrofit2.Call
-import retrofit2.Callback
-import retrofit2.HttpException
-import retrofit2.Response
-import java.lang.reflect.Type
-
-/**
- * The integer code value for a "No Content" response.
- */
-private const val NO_CONTENT_RESPONSE_CODE: Int = 204
-
-/**
- * A [Call] for wrapping a network request into a [Result].
- */
-@Suppress("TooManyFunctions")
-class ResultCall<T>(
-    private val backingCall: Call<T>,
-    private val successType: Type,
-) : Call<Result<T>> {
-    override fun cancel(): Unit = backingCall.cancel()
-
-    override fun clone(): Call<Result<T>> = ResultCall(backingCall, successType)
-
-    override fun enqueue(callback: Callback<Result<T>>): Unit = backingCall.enqueue(
-        object : Callback<T> {
-            override fun onResponse(call: Call<T>, response: Response<T>) {
-                callback.onResponse(this@ResultCall, Response.success(response.toResult()))
-            }
-
-            override fun onFailure(call: Call<T>, t: Throwable) {
-                callback.onResponse(this@ResultCall, Response.success(t.toFailure()))
-            }
-        },
-    )
-
-    @Suppress("TooGenericExceptionCaught")
-    override fun execute(): Response<Result<T>> =
-        try {
-            Response.success(
-                backingCall
-                    .execute()
-                    .toResult(),
-            )
-        } catch (ioException: IOException) {
-            Response.success(ioException.toFailure())
-        } catch (runtimeException: RuntimeException) {
-            Response.success(runtimeException.toFailure())
-        }
-
-    override fun isCanceled(): Boolean = backingCall.isCanceled
-
-    override fun isExecuted(): Boolean = backingCall.isExecuted
-
-    override fun request(): Request = backingCall.request()
-
-    override fun timeout(): Timeout = backingCall.timeout()
-
-    /**
-     * Synchronously send the request and return its response as a [Result].
-     */
-    fun executeForResult(): Result<T> = requireNotNull(execute().body())
-
-    private fun Throwable.toFailure(): Result<T> =
-        this
-            .also {
-                // We rebuild the URL without query params, we do not want to log those
-                val url = backingCall.request().url.toUrl().run { "$protocol://$authority$path" }
-            }
-            .asFailure()
-
-    private fun Response<T>.toResult(): Result<T> =
-        if (!this.isSuccessful) {
-            HttpException(this).toFailure()
-        } else {
-            val body = this.body()
-            @Suppress("UNCHECKED_CAST")
-            when {
-                // We got a nonnull T as the body, just return it.
-                body != null -> body.asSuccess()
-                // We expected the body to be null since the successType is Unit, just return Unit.
-                successType == Unit::class.java -> (Unit as T).asSuccess()
-                // We allow null for 204's, just return null.
-                this.code() == NO_CONTENT_RESPONSE_CODE -> (null as T).asSuccess()
-                // All other null bodies result in an error.
-                else -> IllegalStateException("Unexpected null body!").toFailure()
-            }
-        }
-}
diff --git a/authenticator/src/main/kotlin/com/bitwarden/authenticator/data/platform/datasource/network/core/ResultCallAdapter.kt b/authenticator/src/main/kotlin/com/bitwarden/authenticator/data/platform/datasource/network/core/ResultCallAdapter.kt
deleted file mode 100644
index 801c68a857a..00000000000
--- a/authenticator/src/main/kotlin/com/bitwarden/authenticator/data/platform/datasource/network/core/ResultCallAdapter.kt
+++ /dev/null
@@ -1,16 +0,0 @@
-package com.bitwarden.authenticator.data.platform.datasource.network.core
-
-import retrofit2.Call
-import retrofit2.CallAdapter
-import java.lang.reflect.Type
-
-/**
- * A [CallAdapter] for wrapping network requests into [kotlin.Result].
- */
-class ResultCallAdapter<T>(
-    private val successType: Type,
-) : CallAdapter<T, Call<Result<T>>> {
-
-    override fun responseType(): Type = successType
-    override fun adapt(call: Call<T>): Call<Result<T>> = ResultCall(call, successType)
-}
diff --git a/authenticator/src/main/kotlin/com/bitwarden/authenticator/data/platform/datasource/network/core/ResultCallAdapterFactory.kt b/authenticator/src/main/kotlin/com/bitwarden/authenticator/data/platform/datasource/network/core/ResultCallAdapterFactory.kt
deleted file mode 100644
index 5ef2a41a563..00000000000
--- a/authenticator/src/main/kotlin/com/bitwarden/authenticator/data/platform/datasource/network/core/ResultCallAdapterFactory.kt
+++ /dev/null
@@ -1,32 +0,0 @@
-package com.bitwarden.authenticator.data.platform.datasource.network.core
-
-import retrofit2.Call
-import retrofit2.CallAdapter
-import retrofit2.Retrofit
-import java.lang.reflect.ParameterizedType
-import java.lang.reflect.Type
-
-/**
- * A [CallAdapter.Factory] for wrapping network requests into [kotlin.Result].
- */
-class ResultCallAdapterFactory : CallAdapter.Factory() {
-    override fun get(
-        returnType: Type,
-        annotations: Array<out Annotation>,
-        retrofit: Retrofit,
-    ): CallAdapter<*, *>? {
-        check(returnType is ParameterizedType) { "$returnType must be parameterized" }
-        val containerType = getParameterUpperBound(0, returnType)
-
-        if (getRawType(containerType) != Result::class.java) return null
-        check(containerType is ParameterizedType) { "$containerType must be parameterized" }
-
-        val requestType = getParameterUpperBound(0, containerType)
-
-        return if (getRawType(returnType) == Call::class.java) {
-            ResultCallAdapter<Any>(successType = requestType)
-        } else {
-            null
-        }
-    }
-}
diff --git a/authenticator/src/main/kotlin/com/bitwarden/authenticator/data/platform/datasource/network/retrofit/RetrofitsImpl.kt b/authenticator/src/main/kotlin/com/bitwarden/authenticator/data/platform/datasource/network/retrofit/RetrofitsImpl.kt
index 8dff34effce..ac8bb1e08a8 100644
--- a/authenticator/src/main/kotlin/com/bitwarden/authenticator/data/platform/datasource/network/retrofit/RetrofitsImpl.kt
+++ b/authenticator/src/main/kotlin/com/bitwarden/authenticator/data/platform/datasource/network/retrofit/RetrofitsImpl.kt
@@ -1,9 +1,9 @@
 package com.bitwarden.authenticator.data.platform.datasource.network.retrofit
 
-import com.bitwarden.authenticator.data.platform.datasource.network.core.ResultCallAdapterFactory
 import com.bitwarden.authenticator.data.platform.datasource.network.interceptor.BaseUrlInterceptor
 import com.bitwarden.authenticator.data.platform.datasource.network.interceptor.BaseUrlInterceptors
 import com.bitwarden.authenticator.data.platform.datasource.network.interceptor.HeadersInterceptor
+import com.bitwarden.network.core.NetworkResultCallAdapterFactory
 import kotlinx.serialization.json.Json
 import okhttp3.MediaType.Companion.toMediaType
 import okhttp3.OkHttpClient
@@ -60,7 +60,7 @@ class RetrofitsImpl(
     private val baseRetrofitBuilder: Retrofit.Builder by lazy {
         Retrofit.Builder()
             .addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
-            .addCallAdapterFactory(ResultCallAdapterFactory())
+            .addCallAdapterFactory(NetworkResultCallAdapterFactory())
             .client(baseOkHttpClient)
     }
 
diff --git a/authenticator/src/main/kotlin/com/bitwarden/authenticator/data/platform/datasource/network/service/ConfigServiceImpl.kt b/authenticator/src/main/kotlin/com/bitwarden/authenticator/data/platform/datasource/network/service/ConfigServiceImpl.kt
index 47ec85a70e6..3cfbddfa3f2 100644
--- a/authenticator/src/main/kotlin/com/bitwarden/authenticator/data/platform/datasource/network/service/ConfigServiceImpl.kt
+++ b/authenticator/src/main/kotlin/com/bitwarden/authenticator/data/platform/datasource/network/service/ConfigServiceImpl.kt
@@ -2,10 +2,11 @@ package com.bitwarden.authenticator.data.platform.datasource.network.service
 
 import com.bitwarden.authenticator.data.platform.datasource.network.api.ConfigApi
 import com.bitwarden.authenticator.data.platform.datasource.network.model.ConfigResponseJson
+import com.bitwarden.network.util.toResult
 
 /**
  * Default implementation of [ConfigService] for querying for app configurations.
  */
 class ConfigServiceImpl(private val configApi: ConfigApi) : ConfigService {
-    override suspend fun getConfig(): Result<ConfigResponseJson> = configApi.getConfig()
+    override suspend fun getConfig(): Result<ConfigResponseJson> = configApi.getConfig().toResult()
 }
diff --git a/authenticator/src/test/java/com/bitwarden/authenticator/data/platform/base/BaseServiceTest.kt b/authenticator/src/test/java/com/bitwarden/authenticator/data/platform/base/BaseServiceTest.kt
index 33d7cee96ac..7934661b705 100644
--- a/authenticator/src/test/java/com/bitwarden/authenticator/data/platform/base/BaseServiceTest.kt
+++ b/authenticator/src/test/java/com/bitwarden/authenticator/data/platform/base/BaseServiceTest.kt
@@ -1,7 +1,7 @@
 package com.bitwarden.authenticator.data.platform.base
 
-import com.bitwarden.authenticator.data.platform.datasource.network.core.ResultCallAdapterFactory
 import com.bitwarden.core.di.CoreModule
+import com.bitwarden.network.core.NetworkResultCallAdapterFactory
 import okhttp3.HttpUrl
 import okhttp3.MediaType.Companion.toMediaType
 import okhttp3.mockwebserver.MockWebServer
@@ -24,7 +24,7 @@ abstract class BaseServiceTest {
 
     protected val retrofit: Retrofit = Retrofit.Builder()
         .baseUrl(url.toString())
-        .addCallAdapterFactory(ResultCallAdapterFactory())
+        .addCallAdapterFactory(NetworkResultCallAdapterFactory())
         .addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
         .build()
 
diff --git a/authenticator/src/test/java/com/bitwarden/authenticator/data/platform/datasource/network/core/ResultCallAdapterTest.kt b/authenticator/src/test/java/com/bitwarden/authenticator/data/platform/datasource/network/core/ResultCallAdapterTest.kt
deleted file mode 100644
index 6149e779093..00000000000
--- a/authenticator/src/test/java/com/bitwarden/authenticator/data/platform/datasource/network/core/ResultCallAdapterTest.kt
+++ /dev/null
@@ -1,51 +0,0 @@
-package com.bitwarden.authenticator.data.platform.datasource.network.core
-
-import kotlinx.coroutines.runBlocking
-import okhttp3.mockwebserver.MockResponse
-import okhttp3.mockwebserver.MockWebServer
-import org.junit.jupiter.api.AfterEach
-import org.junit.jupiter.api.Assertions
-import org.junit.jupiter.api.Test
-import retrofit2.Retrofit
-import retrofit2.create
-import retrofit2.http.GET
-
-class ResultCallAdapterTest {
-
-    private val server: MockWebServer = MockWebServer().apply { start() }
-    private val testService: FakeService =
-        Retrofit
-            .Builder()
-            .baseUrl(server.url("/").toString())
-            // add the adapter being tested
-            .addCallAdapterFactory(ResultCallAdapterFactory())
-            .build()
-            .create()
-
-    @AfterEach
-    fun after() {
-        server.shutdown()
-    }
-
-    @Test
-    fun `when server returns error response code result should be failure`() = runBlocking {
-        server.enqueue(MockResponse().setResponseCode(500))
-        val result = testService.requestWithUnitData()
-        Assertions.assertTrue(result.isFailure)
-    }
-
-    @Test
-    fun `when server returns successful response result should be success`() = runBlocking {
-        server.enqueue(MockResponse())
-        val result = testService.requestWithUnitData()
-        Assertions.assertTrue(result.isSuccess)
-    }
-}
-
-/**
- * Fake retrofit service used for testing call adapters.
- */
-private interface FakeService {
-    @GET("/fake")
-    suspend fun requestWithUnitData(): Result<Unit>
-}
diff --git a/authenticator/src/test/java/com/bitwarden/authenticator/data/platform/datasource/network/retrofit/RetrofitsTest.kt b/authenticator/src/test/java/com/bitwarden/authenticator/data/platform/datasource/network/retrofit/RetrofitsTest.kt
index 1518ac1029b..2191cd6a066 100644
--- a/authenticator/src/test/java/com/bitwarden/authenticator/data/platform/datasource/network/retrofit/RetrofitsTest.kt
+++ b/authenticator/src/test/java/com/bitwarden/authenticator/data/platform/datasource/network/retrofit/RetrofitsTest.kt
@@ -2,6 +2,7 @@ package com.bitwarden.authenticator.data.platform.datasource.network.retrofit
 
 import com.bitwarden.authenticator.data.platform.datasource.network.interceptor.BaseUrlInterceptors
 import com.bitwarden.authenticator.data.platform.datasource.network.interceptor.HeadersInterceptor
+import com.bitwarden.network.model.NetworkResult
 import io.mockk.every
 import io.mockk.mockk
 import io.mockk.slot
@@ -109,7 +110,7 @@ class RetrofitsTest {
 
 interface TestApi {
     @GET("/test")
-    suspend fun test(): Result<JsonObject>
+    suspend fun test(): NetworkResult<JsonObject>
 }
 
 /**


Reviews:
user: login: david-livefront
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-02T18:22:59Z
user: login: SaintPatrck
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T18:33:38Z
user: login: david-livefront
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-02T18:41:55Z

Review comments:
diff_hunk: @@ -9,5 +10,5 @@ import retrofit2.http.GET
 interface ConfigApi {
 
     @GET("config")
-    suspend fun getConfig(): Result<ConfigResponseJson>
+    suspend fun getConfig(): NetworkResult<ConfigResponseJson>
path: authenticator/src/main/kotlin/com/bitwarden/authenticator/data/platform/datasource/network/api/ConfigApi.kt
user: login: david-livefront
body: Is this the only API in the authenticator?
created_at: 2025-04-02T18:22:58Z
author_association: COLLABORATOR
diff_hunk: @@ -9,5 +10,5 @@ import retrofit2.http.GET
 interface ConfigApi {
 
     @GET("config")
-    suspend fun getConfig(): Result<ConfigResponseJson>
+    suspend fun getConfig(): NetworkResult<ConfigResponseJson>
path: authenticator/src/main/kotlin/com/bitwarden/authenticator/data/platform/datasource/network/api/ConfigApi.kt
user: login: SaintPatrck
body: Yes
created_at: 2025-04-02T18:33:38Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-04-02T18:27:04Z
body: ![Logo](https://cdn.ast.checkmarx.net/integrations/logo/Checkmarx.png)
**Checkmarx One – Scan Summary & Details** – [b897168d\-f25c\-4c63\-9197\-561a3a4a3b67](https://ast.checkmarx.net/projects/7b48c130-9c07-4ac2-a6dd-15ed4933d967/scans?id=b897168d-f25c-4c63-9197-561a3a4a3b67&branch=network-module%2Fmigrate-authenticator-to-networkresult)
### Great job, no security vulnerabilities found in this Pull Request

author_association: CONTRIBUTOR
user: login: codecov[bot]
created_at: 2025-04-02T18:44:14Z
body: ## [Codecov](https://app.codecov.io/gh/bitwarden/android/pull/4968?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bitwarden) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 81.50%. Comparing base [(`ce13962`)](https://app.codecov.io/gh/bitwarden/android/commit/ce139623d6b8211dfccea42c06f961c29865df5a?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bitwarden) to head [(`b2cf243`)](https://app.codecov.io/gh/bitwarden/android/commit/b2cf243aa3a606bba148ec5af8248af042ef6b3d?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bitwarden).
> Report is 1 commits behind head on main.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #4968      +/-   ##
==========================================
- Coverage   83.96%   81.50%   -2.47%     
==========================================
  Files         598     1099     +501     
  Lines       48333    59127   +10794     
  Branches     6676     7271     +595     
==========================================
+ Hits        40582    48189    +7607     
- Misses       5490     8386    +2896     
- Partials     2261     2552     +291     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/bitwarden/android/pull/4968?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bitwarden).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bitwarden).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE