Backend, sync, infra, docs: ETag, API versioning, k8s, web scaffold, Android 16, domain stubs
- Backend: ShallowEtagHeaderFilter for /api/v1/*, API-VERSIONING.md, README (tenant, CORS, Flyway, ETag) - k8s: backend-deployment.yaml (Deployment, Service, Secret/ConfigMap) - Web: scaffold with directory pull, 304 handling, touch-friendly UI - Android 16: ANDROID-16-TARGET.md; BuildConfig STUN/signaling, SMOAApplication configures InfrastructureManager - Domain: CertificateManager revocation stub, ReportService signReports, ZeroTrust/ThreatDetection minimal docs - TODO.md and IMPLEMENTATION_STATUS.md updated; communications README for endpoint config Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -17,6 +17,10 @@ android {
|
||||
versionCode = AppConfig.versionCode
|
||||
versionName = AppConfig.versionName
|
||||
|
||||
buildConfigField("String", "SMOA_BACKEND_BASE_URL", "\"${project.findProperty("smoa.backend.baseUrl") ?: ""}\"")
|
||||
buildConfigField("String", "SMOA_API_KEY", "\"${project.findProperty("smoa.api.key") ?: ""}\"")
|
||||
buildConfigField("String", "SMOA_STUN_URLS", "\"${project.findProperty("smoa.stun.urls") ?: ""}\"")
|
||||
buildConfigField("String", "SMOA_SIGNALING_URLS", "\"${project.findProperty("smoa.signaling.urls") ?: ""}\"")
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
useSupportLibrary = true
|
||||
@@ -45,6 +49,7 @@ android {
|
||||
|
||||
buildFeatures {
|
||||
compose = true
|
||||
buildConfig = true
|
||||
}
|
||||
|
||||
composeOptions {
|
||||
@@ -118,6 +123,10 @@ dependencies {
|
||||
// Coroutines
|
||||
implementation(Dependencies.coroutinesCore)
|
||||
implementation(Dependencies.coroutinesAndroid)
|
||||
// Networking (for BackendSyncAPI)
|
||||
implementation(Dependencies.retrofit)
|
||||
implementation(Dependencies.retrofitGson)
|
||||
implementation(Dependencies.okHttp)
|
||||
|
||||
// Testing
|
||||
testImplementation(Dependencies.junit)
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
<!-- Network permissions -->
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<!-- Optional: improves 4G/5G/5G MW detection for smart routing (API 29+). Not required for basic connectivity. -->
|
||||
<uses-permission android:name="android.permission.READ_BASIC_PHONE_STATE" />
|
||||
|
||||
<!-- Biometric permissions -->
|
||||
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||
@@ -45,7 +47,8 @@
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.SMOA"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:screenOrientation="fullSensor">
|
||||
android:screenOrientation="fullSensor"
|
||||
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
|
||||
@@ -1,12 +1,28 @@
|
||||
package com.smoa
|
||||
|
||||
import android.app.Application
|
||||
import com.smoa.modules.communications.domain.InfrastructureManager
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltAndroidApp
|
||||
class SMOAApplication : Application() {
|
||||
|
||||
@Inject
|
||||
lateinit var infrastructureManager: InfrastructureManager
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
configureInfrastructure()
|
||||
}
|
||||
|
||||
private fun configureInfrastructure() {
|
||||
BuildConfig.SMOA_STUN_URLS.trim().split(",").map { it.trim() }.filter { it.isNotEmpty() }
|
||||
.takeIf { it.isNotEmpty() }
|
||||
?.let { infrastructureManager.setStunEndpoints(it) }
|
||||
BuildConfig.SMOA_SIGNALING_URLS.trim().split(",").map { it.trim() }.filter { it.isNotEmpty() }
|
||||
.takeIf { it.isNotEmpty() }
|
||||
?.let { infrastructureManager.setSignalingEndpoints(it) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
68
app/src/main/java/com/smoa/api/BackendPullAPI.kt
Normal file
68
app/src/main/java/com/smoa/api/BackendPullAPI.kt
Normal file
@@ -0,0 +1,68 @@
|
||||
package com.smoa.api
|
||||
|
||||
import com.smoa.core.common.PullAPI
|
||||
import com.smoa.core.common.Result
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import retrofit2.HttpException
|
||||
|
||||
class BackendPullAPI(
|
||||
private val apiService: BackendPullApiService,
|
||||
private val apiKey: String? = null
|
||||
) : PullAPI {
|
||||
|
||||
override suspend fun pullDirectory(unit: String?) = withContext(Dispatchers.IO) {
|
||||
runCatching {
|
||||
val r = apiService.getDirectory(unit, unit, apiKey)
|
||||
if (!r.isSuccessful) throw HttpException(r)
|
||||
r.body()?.bytes() ?: ByteArray(0)
|
||||
}.fold(
|
||||
onSuccess = { Result.Success(it) },
|
||||
onFailure = { Result.Error(it) }
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun pullOrders(since: Long?, limit: Int, jurisdiction: String?) = withContext(Dispatchers.IO) {
|
||||
runCatching {
|
||||
val r = apiService.getOrders(since, limit, jurisdiction, jurisdiction, apiKey)
|
||||
if (!r.isSuccessful) throw HttpException(r)
|
||||
r.body()?.bytes() ?: ByteArray(0)
|
||||
}.fold(
|
||||
onSuccess = { Result.Success(it) },
|
||||
onFailure = { Result.Error(it) }
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun pullEvidence(since: Long?, limit: Int, caseNumber: String?) = withContext(Dispatchers.IO) {
|
||||
runCatching {
|
||||
val r = apiService.getEvidence(since, limit, caseNumber, apiKey)
|
||||
if (!r.isSuccessful) throw HttpException(r)
|
||||
r.body()?.bytes() ?: ByteArray(0)
|
||||
}.fold(
|
||||
onSuccess = { Result.Success(it) },
|
||||
onFailure = { Result.Error(it) }
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun pullCredentials(since: Long?, limit: Int, holderId: String?) = withContext(Dispatchers.IO) {
|
||||
runCatching {
|
||||
val r = apiService.getCredentials(since, limit, holderId, apiKey)
|
||||
if (!r.isSuccessful) throw HttpException(r)
|
||||
r.body()?.bytes() ?: ByteArray(0)
|
||||
}.fold(
|
||||
onSuccess = { Result.Success(it) },
|
||||
onFailure = { Result.Error(it) }
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun pullReports(since: Long?, limit: Int) = withContext(Dispatchers.IO) {
|
||||
runCatching {
|
||||
val r = apiService.getReports(since, limit, apiKey)
|
||||
if (!r.isSuccessful) throw HttpException(r)
|
||||
r.body()?.bytes() ?: ByteArray(0)
|
||||
}.fold(
|
||||
onSuccess = { Result.Success(it) },
|
||||
onFailure = { Result.Error(it) }
|
||||
)
|
||||
}
|
||||
}
|
||||
48
app/src/main/java/com/smoa/api/BackendPullApiService.kt
Normal file
48
app/src/main/java/com/smoa/api/BackendPullApiService.kt
Normal file
@@ -0,0 +1,48 @@
|
||||
package com.smoa.api
|
||||
|
||||
import okhttp3.ResponseBody
|
||||
import retrofit2.Response
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Header
|
||||
import retrofit2.http.Query
|
||||
|
||||
interface BackendPullApiService {
|
||||
@GET("api/v1/directory")
|
||||
suspend fun getDirectory(
|
||||
@Query("unit") unit: String?,
|
||||
@Header("X-Unit") xUnit: String?,
|
||||
@Header("X-API-Key") apiKey: String?
|
||||
): Response<ResponseBody>
|
||||
|
||||
@GET("api/v1/orders")
|
||||
suspend fun getOrders(
|
||||
@Query("since") since: Long?,
|
||||
@Query("limit") limit: Int,
|
||||
@Query("jurisdiction") jurisdiction: String?,
|
||||
@Header("X-Unit") xUnit: String?,
|
||||
@Header("X-API-Key") apiKey: String?
|
||||
): Response<ResponseBody>
|
||||
|
||||
@GET("api/v1/evidence")
|
||||
suspend fun getEvidence(
|
||||
@Query("since") since: Long?,
|
||||
@Query("limit") limit: Int,
|
||||
@Query("caseNumber") caseNumber: String?,
|
||||
@Header("X-API-Key") apiKey: String?
|
||||
): Response<ResponseBody>
|
||||
|
||||
@GET("api/v1/credentials")
|
||||
suspend fun getCredentials(
|
||||
@Query("since") since: Long?,
|
||||
@Query("limit") limit: Int,
|
||||
@Query("holderId") holderId: String?,
|
||||
@Header("X-API-Key") apiKey: String?
|
||||
): Response<ResponseBody>
|
||||
|
||||
@GET("api/v1/reports")
|
||||
suspend fun getReports(
|
||||
@Query("since") since: Long?,
|
||||
@Query("limit") limit: Int,
|
||||
@Header("X-API-Key") apiKey: String?
|
||||
): Response<ResponseBody>
|
||||
}
|
||||
163
app/src/main/java/com/smoa/api/BackendSyncAPI.kt
Normal file
163
app/src/main/java/com/smoa/api/BackendSyncAPI.kt
Normal file
@@ -0,0 +1,163 @@
|
||||
package com.smoa.api
|
||||
|
||||
import com.smoa.api.dto.SyncResponseDto
|
||||
import com.smoa.core.common.Result
|
||||
import com.smoa.core.common.SyncResponse
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import retrofit2.HttpException
|
||||
import java.util.Base64
|
||||
|
||||
/**
|
||||
* SyncAPI implementation that calls the SMOA backend over HTTP.
|
||||
* Use when BuildConfig.SMOA_BACKEND_BASE_URL is set.
|
||||
*/
|
||||
class BackendSyncAPI(
|
||||
private val baseUrl: String,
|
||||
private val apiService: BackendSyncApiService,
|
||||
private val apiKey: String? = null
|
||||
) : com.smoa.core.common.SyncAPI {
|
||||
|
||||
private val jsonType = "application/json; charset=utf-8".toMediaType()
|
||||
|
||||
override suspend fun syncDirectoryEntry(entryData: ByteArray): Result<SyncResponse> =
|
||||
withContext(Dispatchers.IO) {
|
||||
runCatching {
|
||||
val body = entryData.toRequestBody(jsonType)
|
||||
val response = apiService.syncDirectory(body, apiKey)
|
||||
mapResponse(response)
|
||||
}.fold(
|
||||
onSuccess = { it },
|
||||
onFailure = { Result.Error(it) }
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun syncOrder(orderData: ByteArray): Result<SyncResponse> =
|
||||
withContext(Dispatchers.IO) {
|
||||
runCatching {
|
||||
val body = orderData.toRequestBody(jsonType)
|
||||
val response = apiService.syncOrder(body, apiKey)
|
||||
mapResponse(response)
|
||||
}.fold(
|
||||
onSuccess = { it },
|
||||
onFailure = { Result.Error(it) }
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun syncEvidence(evidenceData: ByteArray): Result<SyncResponse> =
|
||||
withContext(Dispatchers.IO) {
|
||||
runCatching {
|
||||
val body = evidenceData.toRequestBody(jsonType)
|
||||
val response = apiService.syncEvidence(body, apiKey)
|
||||
mapResponse(response)
|
||||
}.fold(
|
||||
onSuccess = { it },
|
||||
onFailure = { Result.Error(it) }
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun syncCredential(credentialData: ByteArray): Result<SyncResponse> =
|
||||
withContext(Dispatchers.IO) {
|
||||
runCatching {
|
||||
val body = credentialData.toRequestBody(jsonType)
|
||||
val response = apiService.syncCredential(body, apiKey)
|
||||
mapResponse(response)
|
||||
}.fold(
|
||||
onSuccess = { it },
|
||||
onFailure = { Result.Error(it) }
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun syncReport(reportData: ByteArray): Result<SyncResponse> =
|
||||
withContext(Dispatchers.IO) {
|
||||
runCatching {
|
||||
val body = reportData.toRequestBody(jsonType)
|
||||
val response = apiService.syncReport(body, apiKey)
|
||||
mapResponse(response)
|
||||
}.fold(
|
||||
onSuccess = { it },
|
||||
onFailure = { Result.Error(it) }
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun deleteDirectory(id: String): Result<SyncResponse> =
|
||||
withContext(Dispatchers.IO) {
|
||||
runCatching {
|
||||
val response = apiService.deleteDirectory(id, apiKey)
|
||||
mapResponse(response)
|
||||
}.fold(
|
||||
onSuccess = { it },
|
||||
onFailure = { Result.Error(it) }
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun deleteOrder(orderId: String): Result<SyncResponse> =
|
||||
withContext(Dispatchers.IO) {
|
||||
runCatching {
|
||||
val response = apiService.deleteOrder(orderId, apiKey)
|
||||
mapResponse(response)
|
||||
}.fold(
|
||||
onSuccess = { it },
|
||||
onFailure = { Result.Error(it) }
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun deleteEvidence(evidenceId: String): Result<SyncResponse> =
|
||||
withContext(Dispatchers.IO) {
|
||||
runCatching {
|
||||
val response = apiService.deleteEvidence(evidenceId, apiKey)
|
||||
mapResponse(response)
|
||||
}.fold(
|
||||
onSuccess = { it },
|
||||
onFailure = { Result.Error(it) }
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun deleteCredential(credentialId: String): Result<SyncResponse> =
|
||||
withContext(Dispatchers.IO) {
|
||||
runCatching {
|
||||
val response = apiService.deleteCredential(credentialId, apiKey)
|
||||
mapResponse(response)
|
||||
}.fold(
|
||||
onSuccess = { it },
|
||||
onFailure = { Result.Error(it) }
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun deleteReport(reportId: String): Result<SyncResponse> =
|
||||
withContext(Dispatchers.IO) {
|
||||
runCatching {
|
||||
val response = apiService.deleteReport(reportId, apiKey)
|
||||
mapResponse(response)
|
||||
}.fold(
|
||||
onSuccess = { it },
|
||||
onFailure = { Result.Error(it) }
|
||||
)
|
||||
}
|
||||
|
||||
private fun mapResponse(response: retrofit2.Response<SyncResponseDto>): Result<SyncResponse> {
|
||||
if (!response.isSuccessful) {
|
||||
return Result.Error(HttpException(response))
|
||||
}
|
||||
val dto = response.body() ?: return Result.Error(NullPointerException("Empty body"))
|
||||
val remoteData = dto.remoteDataBase64?.let { base64 ->
|
||||
try {
|
||||
Base64.getDecoder().decode(base64)
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
return Result.Success(
|
||||
SyncResponse(
|
||||
success = dto.success,
|
||||
itemId = dto.itemId,
|
||||
serverTimestamp = dto.serverTimestamp,
|
||||
conflict = dto.conflict,
|
||||
remoteData = remoteData,
|
||||
message = dto.message
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
77
app/src/main/java/com/smoa/api/BackendSyncApiService.kt
Normal file
77
app/src/main/java/com/smoa/api/BackendSyncApiService.kt
Normal file
@@ -0,0 +1,77 @@
|
||||
package com.smoa.api
|
||||
|
||||
import com.smoa.api.dto.SyncResponseDto
|
||||
import okhttp3.RequestBody
|
||||
import retrofit2.Response
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.DELETE
|
||||
import retrofit2.http.Header
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.Path
|
||||
|
||||
/**
|
||||
* Retrofit service for SMOA backend sync and delete endpoints.
|
||||
* Base URL is set when building the Retrofit instance.
|
||||
*/
|
||||
interface BackendSyncApiService {
|
||||
|
||||
@POST("api/v1/sync/directory")
|
||||
suspend fun syncDirectory(
|
||||
@Body body: RequestBody,
|
||||
@Header("X-API-Key") apiKey: String?
|
||||
): Response<SyncResponseDto>
|
||||
|
||||
@POST("api/v1/sync/order")
|
||||
suspend fun syncOrder(
|
||||
@Body body: RequestBody,
|
||||
@Header("X-API-Key") apiKey: String?
|
||||
): Response<SyncResponseDto>
|
||||
|
||||
@POST("api/v1/sync/evidence")
|
||||
suspend fun syncEvidence(
|
||||
@Body body: RequestBody,
|
||||
@Header("X-API-Key") apiKey: String?
|
||||
): Response<SyncResponseDto>
|
||||
|
||||
@POST("api/v1/sync/credential")
|
||||
suspend fun syncCredential(
|
||||
@Body body: RequestBody,
|
||||
@Header("X-API-Key") apiKey: String?
|
||||
): Response<SyncResponseDto>
|
||||
|
||||
@POST("api/v1/sync/report")
|
||||
suspend fun syncReport(
|
||||
@Body body: RequestBody,
|
||||
@Header("X-API-Key") apiKey: String?
|
||||
): Response<SyncResponseDto>
|
||||
|
||||
@DELETE("api/v1/sync/directory/{id}")
|
||||
suspend fun deleteDirectory(
|
||||
@Path("id") id: String,
|
||||
@Header("X-API-Key") apiKey: String?
|
||||
): Response<SyncResponseDto>
|
||||
|
||||
@DELETE("api/v1/sync/order/{orderId}")
|
||||
suspend fun deleteOrder(
|
||||
@Path("orderId") orderId: String,
|
||||
@Header("X-API-Key") apiKey: String?
|
||||
): Response<SyncResponseDto>
|
||||
|
||||
@DELETE("api/v1/sync/evidence/{evidenceId}")
|
||||
suspend fun deleteEvidence(
|
||||
@Path("evidenceId") evidenceId: String,
|
||||
@Header("X-API-Key") apiKey: String?
|
||||
): Response<SyncResponseDto>
|
||||
|
||||
@DELETE("api/v1/sync/credential/{credentialId}")
|
||||
suspend fun deleteCredential(
|
||||
@Path("credentialId") credentialId: String,
|
||||
@Header("X-API-Key") apiKey: String?
|
||||
): Response<SyncResponseDto>
|
||||
|
||||
@DELETE("api/v1/sync/report/{reportId}")
|
||||
suspend fun deleteReport(
|
||||
@Path("reportId") reportId: String,
|
||||
@Header("X-API-Key") apiKey: String?
|
||||
): Response<SyncResponseDto>
|
||||
}
|
||||
15
app/src/main/java/com/smoa/api/dto/SyncResponseDto.kt
Normal file
15
app/src/main/java/com/smoa/api/dto/SyncResponseDto.kt
Normal file
@@ -0,0 +1,15 @@
|
||||
package com.smoa.api.dto
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
/**
|
||||
* Backend sync response DTO. remoteData is base64-encoded when conflict=true.
|
||||
*/
|
||||
data class SyncResponseDto(
|
||||
val success: Boolean,
|
||||
val itemId: String,
|
||||
val serverTimestamp: Long,
|
||||
@SerializedName("conflict") val conflict: Boolean = false,
|
||||
@SerializedName("remoteData") val remoteDataBase64: String? = null,
|
||||
val message: String? = null
|
||||
)
|
||||
63
app/src/main/java/com/smoa/di/AppModule.kt
Normal file
63
app/src/main/java/com/smoa/di/AppModule.kt
Normal file
@@ -0,0 +1,63 @@
|
||||
package com.smoa.di
|
||||
|
||||
import com.smoa.api.BackendPullAPI
|
||||
import com.smoa.api.BackendPullApiService
|
||||
import com.smoa.api.BackendSyncAPI
|
||||
import com.smoa.api.BackendSyncApiService
|
||||
import com.smoa.core.common.DefaultPullAPI
|
||||
import com.smoa.core.common.DefaultSyncAPI
|
||||
import com.smoa.core.common.PullAPI
|
||||
import com.smoa.core.common.SyncAPI
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import okhttp3.OkHttpClient
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* App-level bindings. Provides SyncAPI and PullAPI for SyncService.
|
||||
* When SMOA_BACKEND_BASE_URL is set, backend implementations are used.
|
||||
* Build with -Psmoa.backend.baseUrl=http://10.0.2.2:8080/ and -Psmoa.api.key=key for emulator.
|
||||
*/
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object AppModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSyncAPI(): SyncAPI {
|
||||
val (baseUrl, apiKey, retrofit) = createRetrofitIfConfigured() ?: return DefaultSyncAPI()
|
||||
val service = retrofit.create(BackendSyncApiService::class.java)
|
||||
return BackendSyncAPI(baseUrl, service, apiKey = apiKey)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providePullAPI(): PullAPI {
|
||||
val (_, apiKey, retrofit) = createRetrofitIfConfigured() ?: return DefaultPullAPI()
|
||||
val service = retrofit.create(BackendPullApiService::class.java)
|
||||
return BackendPullAPI(service, apiKey = apiKey)
|
||||
}
|
||||
|
||||
private fun createRetrofitIfConfigured(): Triple<String, String?, Retrofit>? {
|
||||
val raw = com.smoa.BuildConfig.SMOA_BACKEND_BASE_URL.trim()
|
||||
val baseUrl = if (raw.isEmpty()) null else (if (raw.endsWith("/")) raw else "$raw/")
|
||||
val apiKey = com.smoa.BuildConfig.SMOA_API_KEY?.trim()?.takeIf { it.isNotEmpty() }
|
||||
if (baseUrl == null) return null
|
||||
val client = OkHttpClient.Builder()
|
||||
.connectTimeout(30, TimeUnit.SECONDS)
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
.writeTimeout(30, TimeUnit.SECONDS)
|
||||
.build()
|
||||
val retrofit = Retrofit.Builder()
|
||||
.baseUrl(baseUrl)
|
||||
.client(client)
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.build()
|
||||
return Triple(baseUrl, apiKey, retrofit)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user