Skip to content

Commit

Permalink
feat(all): Add ability to use OkHttp4 with Amplify v2.x (#2970)
Browse files Browse the repository at this point in the history
  • Loading branch information
mattcreaser authored Jan 15, 2025
1 parent 97ddb8b commit fe0c468
Show file tree
Hide file tree
Showing 17 changed files with 159 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import com.amplifyframework.pinpoint.core.data.AndroidAppDetails
import com.amplifyframework.pinpoint.core.data.AndroidDeviceDetails
import com.amplifyframework.pinpoint.core.database.PinpointDatabase
import com.amplifyframework.pinpoint.core.util.getUniqueId
import com.amplifyframework.util.setHttpEngine

/**
* PinpointManager is the entry point to Pinpoint Analytics and Targeting.
Expand All @@ -36,6 +37,7 @@ internal class PinpointManager constructor(
val analyticsClient: AnalyticsClient
val targetingClient: TargetingClient
internal val pinpointClient: PinpointClient = PinpointClient {
setHttpEngine()
credentialsProvider = this@PinpointManager.credentialsProvider
region = awsPinpointConfiguration.region
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import aws.sdk.kotlin.services.cognitoidentityprovider.endpoints.CognitoIdentity
import aws.smithy.kotlin.runtime.client.RequestInterceptorContext
import aws.smithy.kotlin.runtime.client.endpoints.Endpoint
import aws.smithy.kotlin.runtime.http.interceptors.HttpInterceptor
import com.amplifyframework.util.setHttpEngine

interface AWSCognitoAuthService {
val cognitoIdentityProviderClient: CognitoIdentityProviderClient?
Expand All @@ -33,6 +34,8 @@ interface AWSCognitoAuthService {
val customPairs: MutableMap<String, String> = mutableMapOf()
val cognitoIdentityProviderClient = configuration.userPool?.let { it ->
CognitoIdentityProviderClient {
setHttpEngine()

this.region = it.region
this.endpointProvider = it.endpoint?.let { endpoint ->
CognitoIdentityProviderEndpointProvider { Endpoint(endpoint) }
Expand All @@ -50,6 +53,8 @@ interface AWSCognitoAuthService {

val cognitoIdentityClient = configuration.identityPool?.let { it ->
CognitoIdentityClient {
setHttpEngine()

this.region = it.region
this.interceptors += object : HttpInterceptor {
override suspend fun modifyBeforeSerialization(context: RequestInterceptorContext<Any>): Any {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,16 @@ import aws.smithy.kotlin.runtime.http.interceptors.HttpInterceptor
import com.amplifyframework.auth.AWSCognitoAuthMetadataType
import com.amplifyframework.auth.plugins.core.data.AWSCognitoIdentityPoolConfiguration
import com.amplifyframework.plugins.core.BuildConfig
import com.amplifyframework.util.setHttpEngine

internal object CognitoClientFactory {
fun createIdentityClient(
identityPool: AWSCognitoIdentityPoolConfiguration,
pluginKey: String,
pluginVersion: String,
) = CognitoIdentityClient {
setHttpEngine()

this.region = identityPool.region
this.interceptors += object : HttpInterceptor {
override suspend fun modifyBeforeSerialization(context: RequestInterceptorContext<Any>): Any {
Expand Down
3 changes: 3 additions & 0 deletions aws-core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ dependencies {
implementation(libs.kotlin.stdlib)
implementation(libs.kotlin.coroutines)

implementation(libs.aws.smithy.http)
compileOnly(libs.aws.smithy.okhttp4)

implementation(libs.aws.credentials)
// slf4j dependency is added to fix https://github.com/awslabs/aws-sdk-kotlin/issues/993#issuecomment-1678885524
implementation(libs.slf4j)
Expand Down
59 changes: 59 additions & 0 deletions aws-core/src/main/java/com/amplifyframework/util/AmplifyHttp.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

package com.amplifyframework.util

import aws.smithy.kotlin.runtime.http.config.HttpClientConfig
import aws.smithy.kotlin.runtime.http.engine.okhttp4.OkHttp4Engine
import com.amplifyframework.annotations.InternalAmplifyApi
import com.amplifyframework.core.Amplify

internal object AmplifyHttp {

enum class Version {
OkHttp4,
OkHttp5
}

private val logger = Amplify.Logging.logger("HttpEngine")

val availableVersion: Version by lazy {
// Check to see if OkHttp4 Engine is available on the runtime classpath. If it is then the customer has
// explicitly added it, so we can use that. Otherwise, use the OkHttp5 engine.
try {
Class.forName("aws.smithy.kotlin.runtime.http.engine.okhttp4.OkHttp4Engine")
logger.info("Using OkHttp4 Engine")
Version.OkHttp4
} catch (e: ClassNotFoundException) {
logger.info("Using default OkHttp5 Engine")
Version.OkHttp5
}
}
}

/**
* This function is used to determine, at runtime, whether we should use the OkHttp4Engine instead of the
* default OkHttp5Engine with Kotlin SDK. This allows customers that cannot use OkHttp5 (which is currently an alpha
* release) to use OkHttp4 throughout Amplify by adding a dependency on aws.smithy.kotlin:http-client-engine-okhttp4
* to their runtime classpath.
* This must be called when instantiating any Client instance from the Kotlin SDK.
*/
@InternalAmplifyApi
fun HttpClientConfig.Builder.setHttpEngine() {
// The default engine is OkHttp5. If we should use OkHttp4 instead then override it here.
if (AmplifyHttp.availableVersion == AmplifyHttp.Version.OkHttp4) {
this.httpClient = OkHttp4Engine()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,24 +26,20 @@ import com.amplifyframework.geo.models.Coordinates
import com.amplifyframework.geo.models.CountryCode
import com.amplifyframework.geo.models.Place
import com.amplifyframework.geo.models.SearchArea
import com.amplifyframework.util.setHttpEngine

/**
* Implements the backend provider for the location plugin using
* AWS Kotlin SDK's [LocationClient].
* @param credentialsProvider AWS credentials provider for authorizing API calls
* @param region AWS region for the Amazon Location Service
*/
internal class AmazonLocationService(
credentialsProvider: CredentialsProvider,
region: String
) : GeoService<LocationClient> {
override val provider: LocationClient

init {
provider = LocationClient.invoke {
this.credentialsProvider = credentialsProvider
this.region = region
}
internal class AmazonLocationService(credentialsProvider: CredentialsProvider, region: String) :
GeoService<LocationClient> {
override val provider: LocationClient = LocationClient {
setHttpEngine()
this.credentialsProvider = credentialsProvider
this.region = region
}

override suspend fun getStyleJson(mapName: String): String {
Expand Down Expand Up @@ -75,17 +71,11 @@ internal class AmazonLocationService(
val response = provider.searchPlaceIndexForText(request)

return response.results
?.mapNotNull { it.place }
?.map {
AmazonLocationPlace(it)
} ?: listOf()
.mapNotNull { it.place }
.map { AmazonLocationPlace(it) }
}

override suspend fun reverseGeocode(
index: String,
position: Coordinates,
limit: Int
): List<Place> {
override suspend fun reverseGeocode(index: String, position: Coordinates, limit: Int): List<Place> {
val request = SearchPlaceIndexForPositionRequest.invoke {
this.position = listOf(position.longitude, position.latitude)
indexName = index
Expand All @@ -94,9 +84,7 @@ internal class AmazonLocationService(
val response = provider.searchPlaceIndexForPosition(request)

return response.results
?.mapNotNull { it.place }
?.map {
AmazonLocationPlace(it)
} ?: listOf()
.mapNotNull { it.place }
.map { AmazonLocationPlace(it) }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ import com.amplifyframework.logging.LoggingPlugin
import com.amplifyframework.logging.cloudwatch.models.AWSCloudWatchLoggingPluginConfiguration
import com.amplifyframework.logging.cloudwatch.worker.CloudwatchRouterWorker
import com.amplifyframework.logging.cloudwatch.worker.CloudwatchWorkerFactory
import com.amplifyframework.util.setHttpEngine
import java.net.URL
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import org.json.JSONObject

Expand Down Expand Up @@ -96,6 +96,7 @@ class AWSCloudWatchLoggingPlugin @JvmOverloads constructor(
val awsLoggingConfig = awsCloudWatchLoggingPluginConfig ?: getConfigFromFile(pluginConfiguration)
loggingConstraintsResolver.context = context
cloudWatchLogsClient = CloudWatchLogsClient {
setHttpEngine()
credentialsProvider = CognitoCredentialsProvider()
region = awsLoggingConfig.region
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ import com.amplifyframework.predictions.models.Sentiment
import com.amplifyframework.predictions.models.SentimentType
import com.amplifyframework.predictions.models.Syntax
import com.amplifyframework.predictions.result.InterpretResult
import java.util.ArrayList
import com.amplifyframework.util.setHttpEngine
import java.util.concurrent.Executors
import kotlinx.coroutines.runBlocking

Expand All @@ -52,6 +52,7 @@ internal class AWSComprehendService(
private val authCredentialsProvider: CredentialsProvider
) {
val client: ComprehendClient = ComprehendClient {
setHttpEngine()
this.region = pluginConfiguration.defaultRegion
this.credentialsProvider = authCredentialsProvider
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import com.amplifyframework.predictions.PredictionsException
import com.amplifyframework.predictions.aws.AWSPredictionsPluginConfiguration
import com.amplifyframework.predictions.aws.models.AWSVoiceType
import com.amplifyframework.predictions.result.TextToSpeechResult
import com.amplifyframework.util.setHttpEngine
import java.io.InputStream
import java.util.concurrent.Executors
import kotlinx.coroutines.runBlocking
Expand All @@ -40,6 +41,7 @@ internal class AWSPollyService(
) {
val client: PollyClient = AmazonPollyPresigningClient(
PollyClient {
setHttpEngine()
this.region = pluginConfiguration.defaultRegion
this.credentialsProvider = authCredentialsProvider
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ import com.amplifyframework.predictions.result.IdentifyEntityMatchesResult
import com.amplifyframework.predictions.result.IdentifyLabelsResult
import com.amplifyframework.predictions.result.IdentifyResult
import com.amplifyframework.predictions.result.IdentifyTextResult
import java.lang.StringBuilder
import com.amplifyframework.util.setHttpEngine
import java.net.MalformedURLException
import java.net.URL
import java.nio.ByteBuffer
Expand All @@ -64,6 +64,7 @@ internal class AWSRekognitionService(
) {

val client: RekognitionClient = RekognitionClient {
setHttpEngine()
this.region = pluginConfiguration.defaultRegion
this.credentialsProvider = authCredentialsProvider
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,8 @@ import com.amplifyframework.predictions.models.Table
import com.amplifyframework.predictions.models.TextFormatType
import com.amplifyframework.predictions.result.IdentifyDocumentTextResult
import com.amplifyframework.predictions.result.IdentifyResult
import java.lang.StringBuilder
import com.amplifyframework.util.setHttpEngine
import java.nio.ByteBuffer
import java.util.ArrayList
import java.util.HashMap
import java.util.concurrent.Executors
import kotlinx.coroutines.runBlocking

Expand All @@ -47,6 +45,7 @@ internal class AWSTextractService(
private val authCredentialsProvider: CredentialsProvider
) {
val client: TextractClient = TextractClient {
setHttpEngine()
this.region = pluginConfiguration.defaultRegion
this.credentialsProvider = authCredentialsProvider
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import com.amplifyframework.predictions.PredictionsException
import com.amplifyframework.predictions.aws.AWSPredictionsPluginConfiguration
import com.amplifyframework.predictions.models.LanguageType
import com.amplifyframework.predictions.result.TranslateTextResult
import com.amplifyframework.util.setHttpEngine
import java.util.concurrent.Executors
import kotlinx.coroutines.runBlocking

Expand All @@ -33,6 +34,7 @@ internal class AWSTranslateService(
private val authCredentialsProvider: CredentialsProvider
) {
val client: TranslateClient = TranslateClient {
setHttpEngine()
this.region = pluginConfiguration.defaultRegion
this.credentialsProvider = authCredentialsProvider
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import com.amplifyframework.pinpoint.core.data.AndroidAppDetails
import com.amplifyframework.pinpoint.core.data.AndroidDeviceDetails
import com.amplifyframework.pinpoint.core.database.PinpointDatabase
import com.amplifyframework.pinpoint.core.util.getUniqueId
import com.amplifyframework.util.setHttpEngine
import com.google.firebase.messaging.FirebaseMessaging
import java.util.concurrent.ConcurrentHashMap
import kotlin.random.Random
Expand Down Expand Up @@ -133,6 +134,7 @@ class AWSPinpointPushNotificationsPlugin : PushNotificationsPlugin<PinpointClien
}

private fun createPinpointClient() = PinpointClient {
setHttpEngine()
region = configuration.region
credentialsProvider = CognitoCredentialsProvider()
interceptors += object : HttpInterceptor {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package com.amplifyframework.storage.s3.transfer

import aws.sdk.kotlin.services.s3.S3Client
import com.amplifyframework.auth.AuthCredentialsProvider
import com.amplifyframework.util.setHttpEngine

internal class S3StorageTransferClientProvider(
private val createS3Client: (region: String?, bucketName: String?) -> S3Client
Expand All @@ -24,6 +25,7 @@ internal class S3StorageTransferClientProvider(
@JvmStatic
fun getS3Client(region: String, authCredentialsProvider: AuthCredentialsProvider): S3Client {
return S3Client {
setHttpEngine()
this.region = region
this.credentialsProvider = authCredentialsProvider
}
Expand Down
3 changes: 3 additions & 0 deletions configuration/consumer-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,6 @@

-keep class com.amazonaws.** { *; }
-keep class com.amplifyframework.** { *; }

# We check for specific engine classes on the classpath to determine whether Amplify should use OkHttp4 instead of OkHttp5
-keepnames class aws.smithy.kotlin.runtime.http.engine.okhttp4.*
54 changes: 54 additions & 0 deletions documents/OkHttp4.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Using OkHttp4 in Amplify Android

Amplify Android v2 uses OkHttp5 by default, but starting with release `2.26.0` it will switch all clients to use OkHttp4 if the `OkHttp4Engine`
is available on the runtime classpath. Please use these steps to switch to using OkHttp4.

## 1. Upgrade Amplify if necessary

You must be using at least Amplify `2.26.0` to use OkHttp4.

## 2. Add the required dependency

Add the dependency on the `OkHttp4Engine` library to your application's `build.gradle.kts`

```kotlin
dependencies {
implementation("aws.smithy.kotlin:http-client-engine-okhttp4:1.3.32") // Version must align with Smithy dependency in Amplify
}
```

To determine the correct version for the above dependency check in Amplify's [libs.versions.toml](../gradle/libs.versions.toml) file.
Ensure that you are viewing the file version for the Amplify version you are using, and then check the version entry for `aws-smithy`.
Remember to keep these versions in sync when you update Amplify.

## 3. Force the OkHttp version

Add the following snippet in your application's `build.gradle.kts` file:

```kotlin
configurations.configureEach {
// Force replace OkHttp5 with OkHttp4
resolutionStrategy {
force("com.squareup.okhttp3:okhttp:4.12.0") // Or whicher OkHttp version you want
}
// Exclude other OkHttp5 dependencies
exclude(group = "com.squareup.okhttp3", module = "okhttp-coroutines")
}
```

## 4. Add Proguard/R8 rules

If you are using obfuscation/minification you may encounter compilation errors in affected builds. Check
`build/outputs/mapping/<variant>/missing_rules.txt` for any rules that are needed. The following
rules may need to be added to `proguard-rules.pro`:

```
-dontwarn com.google.errorprone.annotations.Immutable
-dontwarn okhttp3.ConnectionListener$Companion
-dontwarn okhttp3.ConnectionListener
-dontwarn okhttp3.coroutines.ExecuteAsyncKt
```

## Troubleshooting

Please refer to the [AWS SDK for Kotlin document](https://github.com/smithy-lang/smithy-kotlin/tree/main/runtime/protocol/http-client-engines/http-client-engine-okhttp4) on this topic or [Open an Issue](https://github.com/aws-amplify/amplify-android/issues/new/choose) if you run into any problems.
Loading

0 comments on commit fe0c468

Please sign in to comment.