From 235a367674b657e48775823772a2046f04da8829 Mon Sep 17 00:00:00 2001 From: Renat Kurbanov Date: Thu, 28 Mar 2024 16:50:43 +0700 Subject: [PATCH] fetch fallback host if needed (#83) --- .../com/apphud/sdk/ApphudInternal+Fallback.kt | 56 +++++++++++++++++-- .../java/com/apphud/sdk/client/ApiClient.kt | 2 +- .../sdk/managers/HttpRetryInterceptor.kt | 26 +++++++-- .../com/apphud/sdk/managers/RequestManager.kt | 24 +++++++- 4 files changed, 95 insertions(+), 13 deletions(-) diff --git a/sdk/src/main/java/com/apphud/sdk/ApphudInternal+Fallback.kt b/sdk/src/main/java/com/apphud/sdk/ApphudInternal+Fallback.kt index 7c0e3d90..b1c28fcb 100644 --- a/sdk/src/main/java/com/apphud/sdk/ApphudInternal+Fallback.kt +++ b/sdk/src/main/java/com/apphud/sdk/ApphudInternal+Fallback.kt @@ -2,8 +2,10 @@ package com.apphud.sdk import android.content.Context import android.content.SharedPreferences +import com.apphud.sdk.client.ApiClient import com.apphud.sdk.domain.ApphudUser import com.apphud.sdk.domain.FallbackJsonObject +import com.apphud.sdk.managers.RequestManager import com.apphud.sdk.mappers.PaywallsMapper import com.apphud.sdk.parser.GsonParser import com.apphud.sdk.parser.Parser @@ -14,16 +16,57 @@ import com.google.gson.reflect.TypeToken import kotlinx.coroutines.launch import okhttp3.Request import java.io.IOException +import java.net.MalformedURLException +import java.net.URL + +internal fun String.withRemovedScheme(): String { + return replace("https://", "") +} private val gson = GsonBuilder().serializeNulls().create() private val parser: Parser = GsonParser(gson) private val paywallsMapper = PaywallsMapper(parser) - -internal fun ApphudInternal.processFallbackError(request: Request) { +internal var fallbackHost: String? = null +internal var processedFallbackData = false +internal fun ApphudInternal.processFallbackError(request: Request, isTimeout: Boolean) { if ((request.url.encodedPath.endsWith("/customers") || - request.url.encodedPath.endsWith("/subscriptions")) - && !fallbackMode) { - processFallbackData() + request.url.encodedPath.endsWith("/subscriptions") || + request.url.encodedPath.endsWith("/products")) + && !processedFallbackData) { + + if (fallbackHost?.withRemovedScheme() == request.url.host) { + processFallbackData() + } else { + coroutineScope.launch { + tryFallbackHost() + if (fallbackHost == null) { + processFallbackData() + } + } + } + } +} + +internal fun tryFallbackHost() { + val host = RequestManager.fetchFallbackHost() + host?.let { + if (isValidUrl(it)) { + fallbackHost = it + ApphudInternal.fallbackMode = true + ApiClient.host = fallbackHost!! + ApphudLog.logE("Fallback to host $fallbackHost") + ApphudInternal.isRegisteringUser = false + ApphudInternal.refreshPaywallsIfNeeded() + } + } +} + +fun isValidUrl(urlString: String): Boolean { + return try { + URL(urlString) + true + } catch (e: MalformedURLException) { + false } } @@ -38,6 +81,8 @@ private fun ApphudInternal.processFallbackData() { ApphudLog.log("Fallback: user created: $userId") } + processedFallbackData = true + // read paywalls from cache var ids = paywalls.map { it.products?.map { it.productId } ?: listOf() }.flatten() if (ids.isEmpty()) { @@ -87,6 +132,7 @@ private fun getJsonDataFromAsset( internal fun ApphudInternal.disableFallback() { fallbackMode = false + processedFallbackData = false ApphudLog.log("Fallback: DISABLED") coroutineScope.launch(errorHandler) { if (productGroups.isEmpty()) { // if fallback raised on start, there no product groups, so reload products and details diff --git a/sdk/src/main/java/com/apphud/sdk/client/ApiClient.kt b/sdk/src/main/java/com/apphud/sdk/client/ApiClient.kt index f048d53b..84811220 100644 --- a/sdk/src/main/java/com/apphud/sdk/client/ApiClient.kt +++ b/sdk/src/main/java/com/apphud/sdk/client/ApiClient.kt @@ -1,6 +1,6 @@ package com.apphud.sdk.client object ApiClient { - var host = "https://api.apphud.com" + var host = "https://gateway.apphud.com" var readTimeout: Long = 10L } diff --git a/sdk/src/main/java/com/apphud/sdk/managers/HttpRetryInterceptor.kt b/sdk/src/main/java/com/apphud/sdk/managers/HttpRetryInterceptor.kt index 74b21dca..e93c4bf3 100644 --- a/sdk/src/main/java/com/apphud/sdk/managers/HttpRetryInterceptor.kt +++ b/sdk/src/main/java/com/apphud/sdk/managers/HttpRetryInterceptor.kt @@ -4,7 +4,12 @@ import com.apphud.sdk.ApphudInternal import com.apphud.sdk.ApphudInternal.FALLBACK_ERRORS import com.apphud.sdk.ApphudInternal.fallbackMode import com.apphud.sdk.ApphudLog +import com.apphud.sdk.ApphudUtils +import com.apphud.sdk.fallbackHost import com.apphud.sdk.processFallbackError +import com.apphud.sdk.tryFallbackHost +import com.apphud.sdk.withRemovedScheme +import kotlinx.coroutines.launch import okhttp3.Interceptor import okhttp3.Request import okhttp3.Response @@ -48,22 +53,22 @@ class HttpRetryInterceptor : Interceptor { } if (response.code in FALLBACK_ERRORS) { - ApphudInternal.processFallbackError(request) + ApphudInternal.processFallbackError(request, isTimeout = false) if (ApphudInternal.fallbackMode) { tryCount = MAX_COUNT } } ApphudLog.logE( - "Request (${request.url.encodedPath}) failed with code (${response.code}). Will retry in ${STEP / 1000} seconds ($tryCount).", + "Request (${request.url}) failed with code (${response.code}). Will retry in ${STEP / 1000} seconds ($tryCount).", ) Thread.sleep(STEP) } } catch (e: SocketTimeoutException) { - ApphudInternal.processFallbackError(request) + ApphudInternal.processFallbackError(request, isTimeout = true) ApphudLog.logE( - "Request (${request.url.encodedPath}) failed with SocketTimeoutException. Will retry in ${STEP / 1000} seconds ($tryCount).", + "Request (${request.url}) failed with SocketTimeoutException. Will retry in ${STEP / 1000} seconds ($tryCount).", ) if (ApphudInternal.fallbackMode) { throw e @@ -72,9 +77,14 @@ class HttpRetryInterceptor : Interceptor { } catch (e: UnknownHostException) { // do not retry when no internet connection issue tryCount = MAX_COUNT + if (ApphudUtils.isOnline(ApphudInternal.context)) { + ApphudInternal.coroutineScope.launch { + tryFallbackHost() + } + } } catch (e: Exception) { ApphudLog.logE( - "Request (${request.url.encodedPath}) failed with Exception. Will retry in ${STEP / 1000} seconds ($tryCount).", + "Request (${request.url}) failed with Exception. Will retry in ${STEP / 1000} seconds ($tryCount).", ) Thread.sleep(STEP) } finally { @@ -83,6 +93,12 @@ class HttpRetryInterceptor : Interceptor { if (!isSuccess && tryCount < MAX_COUNT && !(response?.code in 401..403)) { // response?.close() } + + if (fallbackHost != null && fallbackHost?.withRemovedScheme() != request.url.host) { + // invalid host, need to abort these requests + tryCount = MAX_COUNT + throw UnknownHostException("APPHUD_HOST_CHANGED") + } } } if (!isSuccess) { diff --git a/sdk/src/main/java/com/apphud/sdk/managers/RequestManager.kt b/sdk/src/main/java/com/apphud/sdk/managers/RequestManager.kt index 1971c362..fc11cef2 100644 --- a/sdk/src/main/java/com/apphud/sdk/managers/RequestManager.kt +++ b/sdk/src/main/java/com/apphud/sdk/managers/RequestManager.kt @@ -217,7 +217,7 @@ object RequestManager { completionHandler(null, ApphudError(message)) } } catch (e: SocketTimeoutException) { - ApphudInternal.processFallbackError(request) + ApphudInternal.processFallbackError(request, isTimeout = true) val message = e.message ?: "Undefined error" completionHandler(null, ApphudError(message, null, APPHUD_ERROR_TIMEOUT)) } catch (e: IOException) { @@ -399,7 +399,7 @@ object RequestManager { completionHandler(null, ApphudError("Registration failed")) } } catch (e: SocketTimeoutException) { - ApphudInternal.processFallbackError(request) + ApphudInternal.processFallbackError(request, isTimeout = true) val message = e.message ?: "Registration failed" completionHandler(null, ApphudError(message, null, APPHUD_ERROR_TIMEOUT)) } catch (ex: UnknownHostException) { @@ -657,6 +657,26 @@ object RequestManager { } } + fun fetchFallbackHost(): String? { + val url = "https://apphud.blob.core.windows.net/apphud-gateway/fallback.txt" + val client = OkHttpClient() + + // Build the request + val request = Request.Builder() + .url(url) + .build() + + // Execute the request + val response = client.newCall(request).execute() + + // Return the response body as a string if the request was successful + return if (response.isSuccessful) { + response.body?.string() + } else { + null + } + } + fun grantPromotional( daysCount: Int, productId: String?,