From fe0c468256cf927c0529e91504088ff0c5ddff75 Mon Sep 17 00:00:00 2001 From: Matt Creaser Date: Wed, 15 Jan 2025 16:00:29 -0400 Subject: [PATCH 1/3] feat(all): Add ability to use OkHttp4 with Amplify v2.x (#2970) --- .../analytics/pinpoint/PinpointManager.kt | 2 + .../auth/cognito/AWSCognitoAuthService.kt | 5 ++ .../auth/plugins/core/CognitoClientFactory.kt | 3 + aws-core/build.gradle.kts | 3 + .../com/amplifyframework/util/AmplifyHttp.kt | 59 +++++++++++++++++++ .../location/service/AmazonLocationService.kt | 36 ++++------- .../cloudwatch/AWSCloudWatchLoggingPlugin.kt | 3 +- .../aws/service/AWSComprehendService.kt | 3 +- .../aws/service/AWSPollyService.kt | 2 + .../aws/service/AWSRekognitionService.kt | 3 +- .../aws/service/AWSTextractService.kt | 5 +- .../aws/service/AWSTranslateService.kt | 2 + .../AWSPinpointPushNotificationsPlugin.kt | 2 + .../S3StorageTransferClientProvider.kt | 2 + configuration/consumer-rules.pro | 3 + documents/OkHttp4.md | 54 +++++++++++++++++ gradle/libs.versions.toml | 2 + 17 files changed, 159 insertions(+), 30 deletions(-) create mode 100644 aws-core/src/main/java/com/amplifyframework/util/AmplifyHttp.kt create mode 100644 documents/OkHttp4.md diff --git a/aws-analytics-pinpoint/src/main/java/com/amplifyframework/analytics/pinpoint/PinpointManager.kt b/aws-analytics-pinpoint/src/main/java/com/amplifyframework/analytics/pinpoint/PinpointManager.kt index 7a6d8d8cc7..3cefa27d34 100644 --- a/aws-analytics-pinpoint/src/main/java/com/amplifyframework/analytics/pinpoint/PinpointManager.kt +++ b/aws-analytics-pinpoint/src/main/java/com/amplifyframework/analytics/pinpoint/PinpointManager.kt @@ -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. @@ -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 } diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/AWSCognitoAuthService.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/AWSCognitoAuthService.kt index f275e619c6..30018e2b33 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/AWSCognitoAuthService.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/AWSCognitoAuthService.kt @@ -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? @@ -33,6 +34,8 @@ interface AWSCognitoAuthService { val customPairs: MutableMap = mutableMapOf() val cognitoIdentityProviderClient = configuration.userPool?.let { it -> CognitoIdentityProviderClient { + setHttpEngine() + this.region = it.region this.endpointProvider = it.endpoint?.let { endpoint -> CognitoIdentityProviderEndpointProvider { Endpoint(endpoint) } @@ -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 { diff --git a/aws-auth-plugins-core/src/main/java/com/amplifyframework/auth/plugins/core/CognitoClientFactory.kt b/aws-auth-plugins-core/src/main/java/com/amplifyframework/auth/plugins/core/CognitoClientFactory.kt index f0537238ee..a51fca1ba3 100644 --- a/aws-auth-plugins-core/src/main/java/com/amplifyframework/auth/plugins/core/CognitoClientFactory.kt +++ b/aws-auth-plugins-core/src/main/java/com/amplifyframework/auth/plugins/core/CognitoClientFactory.kt @@ -22,6 +22,7 @@ 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( @@ -29,6 +30,8 @@ internal object CognitoClientFactory { pluginKey: String, pluginVersion: String, ) = CognitoIdentityClient { + setHttpEngine() + this.region = identityPool.region this.interceptors += object : HttpInterceptor { override suspend fun modifyBeforeSerialization(context: RequestInterceptorContext): Any { diff --git a/aws-core/build.gradle.kts b/aws-core/build.gradle.kts index 47b7a9e85e..61ebfd06e2 100644 --- a/aws-core/build.gradle.kts +++ b/aws-core/build.gradle.kts @@ -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) diff --git a/aws-core/src/main/java/com/amplifyframework/util/AmplifyHttp.kt b/aws-core/src/main/java/com/amplifyframework/util/AmplifyHttp.kt new file mode 100644 index 0000000000..208d5079a3 --- /dev/null +++ b/aws-core/src/main/java/com/amplifyframework/util/AmplifyHttp.kt @@ -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() + } +} diff --git a/aws-geo-location/src/main/java/com/amplifyframework/geo/location/service/AmazonLocationService.kt b/aws-geo-location/src/main/java/com/amplifyframework/geo/location/service/AmazonLocationService.kt index 2549031e96..1e860dbf73 100644 --- a/aws-geo-location/src/main/java/com/amplifyframework/geo/location/service/AmazonLocationService.kt +++ b/aws-geo-location/src/main/java/com/amplifyframework/geo/location/service/AmazonLocationService.kt @@ -26,6 +26,7 @@ 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 @@ -33,17 +34,12 @@ import com.amplifyframework.geo.models.SearchArea * @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 { - override val provider: LocationClient - - init { - provider = LocationClient.invoke { - this.credentialsProvider = credentialsProvider - this.region = region - } +internal class AmazonLocationService(credentialsProvider: CredentialsProvider, region: String) : + GeoService { + override val provider: LocationClient = LocationClient { + setHttpEngine() + this.credentialsProvider = credentialsProvider + this.region = region } override suspend fun getStyleJson(mapName: String): String { @@ -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 { + override suspend fun reverseGeocode(index: String, position: Coordinates, limit: Int): List { val request = SearchPlaceIndexForPositionRequest.invoke { this.position = listOf(position.longitude, position.latitude) indexName = index @@ -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) } } } diff --git a/aws-logging-cloudwatch/src/main/java/com/amplifyframework/logging/cloudwatch/AWSCloudWatchLoggingPlugin.kt b/aws-logging-cloudwatch/src/main/java/com/amplifyframework/logging/cloudwatch/AWSCloudWatchLoggingPlugin.kt index 7630ca6f09..218838a9b1 100644 --- a/aws-logging-cloudwatch/src/main/java/com/amplifyframework/logging/cloudwatch/AWSCloudWatchLoggingPlugin.kt +++ b/aws-logging-cloudwatch/src/main/java/com/amplifyframework/logging/cloudwatch/AWSCloudWatchLoggingPlugin.kt @@ -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 @@ -96,6 +96,7 @@ class AWSCloudWatchLoggingPlugin @JvmOverloads constructor( val awsLoggingConfig = awsCloudWatchLoggingPluginConfig ?: getConfigFromFile(pluginConfiguration) loggingConstraintsResolver.context = context cloudWatchLogsClient = CloudWatchLogsClient { + setHttpEngine() credentialsProvider = CognitoCredentialsProvider() region = awsLoggingConfig.region } diff --git a/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/service/AWSComprehendService.kt b/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/service/AWSComprehendService.kt index 4007bd0e27..093f0e2f2d 100644 --- a/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/service/AWSComprehendService.kt +++ b/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/service/AWSComprehendService.kt @@ -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 @@ -52,6 +52,7 @@ internal class AWSComprehendService( private val authCredentialsProvider: CredentialsProvider ) { val client: ComprehendClient = ComprehendClient { + setHttpEngine() this.region = pluginConfiguration.defaultRegion this.credentialsProvider = authCredentialsProvider } diff --git a/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/service/AWSPollyService.kt b/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/service/AWSPollyService.kt index 795ee99ae4..0178dfeacd 100644 --- a/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/service/AWSPollyService.kt +++ b/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/service/AWSPollyService.kt @@ -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 @@ -40,6 +41,7 @@ internal class AWSPollyService( ) { val client: PollyClient = AmazonPollyPresigningClient( PollyClient { + setHttpEngine() this.region = pluginConfiguration.defaultRegion this.credentialsProvider = authCredentialsProvider } diff --git a/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/service/AWSRekognitionService.kt b/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/service/AWSRekognitionService.kt index fb072ff73c..80b5e4b0d2 100644 --- a/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/service/AWSRekognitionService.kt +++ b/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/service/AWSRekognitionService.kt @@ -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 @@ -64,6 +64,7 @@ internal class AWSRekognitionService( ) { val client: RekognitionClient = RekognitionClient { + setHttpEngine() this.region = pluginConfiguration.defaultRegion this.credentialsProvider = authCredentialsProvider } diff --git a/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/service/AWSTextractService.kt b/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/service/AWSTextractService.kt index a40deadc21..45dfe32d1d 100644 --- a/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/service/AWSTextractService.kt +++ b/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/service/AWSTextractService.kt @@ -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 @@ -47,6 +45,7 @@ internal class AWSTextractService( private val authCredentialsProvider: CredentialsProvider ) { val client: TextractClient = TextractClient { + setHttpEngine() this.region = pluginConfiguration.defaultRegion this.credentialsProvider = authCredentialsProvider } diff --git a/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/service/AWSTranslateService.kt b/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/service/AWSTranslateService.kt index f542cc1f39..3bff2d250f 100644 --- a/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/service/AWSTranslateService.kt +++ b/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/service/AWSTranslateService.kt @@ -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 @@ -33,6 +34,7 @@ internal class AWSTranslateService( private val authCredentialsProvider: CredentialsProvider ) { val client: TranslateClient = TranslateClient { + setHttpEngine() this.region = pluginConfiguration.defaultRegion this.credentialsProvider = authCredentialsProvider } diff --git a/aws-push-notifications-pinpoint/src/main/java/com/amplifyframework/pushnotifications/pinpoint/AWSPinpointPushNotificationsPlugin.kt b/aws-push-notifications-pinpoint/src/main/java/com/amplifyframework/pushnotifications/pinpoint/AWSPinpointPushNotificationsPlugin.kt index 9e0be9fb9f..05a3afe02c 100644 --- a/aws-push-notifications-pinpoint/src/main/java/com/amplifyframework/pushnotifications/pinpoint/AWSPinpointPushNotificationsPlugin.kt +++ b/aws-push-notifications-pinpoint/src/main/java/com/amplifyframework/pushnotifications/pinpoint/AWSPinpointPushNotificationsPlugin.kt @@ -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 @@ -133,6 +134,7 @@ class AWSPinpointPushNotificationsPlugin : PushNotificationsPlugin S3Client @@ -24,6 +25,7 @@ internal class S3StorageTransferClientProvider( @JvmStatic fun getS3Client(region: String, authCredentialsProvider: AuthCredentialsProvider): S3Client { return S3Client { + setHttpEngine() this.region = region this.credentialsProvider = authCredentialsProvider } diff --git a/configuration/consumer-rules.pro b/configuration/consumer-rules.pro index 24be28fad1..38865c9600 100644 --- a/configuration/consumer-rules.pro +++ b/configuration/consumer-rules.pro @@ -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.* \ No newline at end of file diff --git a/documents/OkHttp4.md b/documents/OkHttp4.md new file mode 100644 index 0000000000..abc83b5be5 --- /dev/null +++ b/documents/OkHttp4.md @@ -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//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. \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8f6cec9446..2b07480351 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -83,6 +83,8 @@ aws-polly = { module = "aws.sdk.kotlin:polly", version.ref = "aws-kotlin" } aws-rekognition = { module = "aws.sdk.kotlin:rekognition", version.ref = "aws-kotlin" } aws-s3 = { module = "aws.sdk.kotlin:s3", version.ref = "aws-kotlin" } aws-signing = { module = "aws.smithy.kotlin:aws-signing-default", version.ref = "aws-smithy" } +aws-smithy-http = { module = "aws.smithy.kotlin:http-client-jvm", version.ref = "aws-smithy" } +aws-smithy-okhttp4 = { module = "aws.smithy.kotlin:http-client-engine-okhttp4", version.ref = "aws-smithy" } aws-textract = { module = "aws.sdk.kotlin:textract", version.ref = "aws-kotlin" } aws-translate = { module = "aws.sdk.kotlin:translate", version.ref = "aws-kotlin" } firebasemessaging = { module = "com.google.firebase:firebase-messaging-ktx", version.ref = "fcm" } From 3e568e729d8e7fc7565dbcebd450400f823d752b Mon Sep 17 00:00:00 2001 From: Tyler Roach Date: Wed, 15 Jan 2025 16:28:27 -0500 Subject: [PATCH 2/3] fix(logging): CloudWatch Plugin 16KB page size support (#2919) Co-authored-by: Vincent Tran --- .../cloudwatch/db/CloudWatchDatabaseHelper.kt | 18 ++++++++++++++---- .../cloudwatch/db/CloudWatchLoggingDatabase.kt | 4 ++-- .../logging/cloudwatch/db/LogEventTable.kt | 2 +- build.gradle.kts | 2 +- gradle/libs.versions.toml | 4 ++-- 5 files changed, 20 insertions(+), 10 deletions(-) diff --git a/aws-logging-cloudwatch/src/main/java/com/amplifyframework/logging/cloudwatch/db/CloudWatchDatabaseHelper.kt b/aws-logging-cloudwatch/src/main/java/com/amplifyframework/logging/cloudwatch/db/CloudWatchDatabaseHelper.kt index e38eecd9eb..4ec33151bb 100644 --- a/aws-logging-cloudwatch/src/main/java/com/amplifyframework/logging/cloudwatch/db/CloudWatchDatabaseHelper.kt +++ b/aws-logging-cloudwatch/src/main/java/com/amplifyframework/logging/cloudwatch/db/CloudWatchDatabaseHelper.kt @@ -15,11 +15,21 @@ package com.amplifyframework.logging.cloudwatch.db import android.content.Context -import net.sqlcipher.database.SQLiteDatabase -import net.sqlcipher.database.SQLiteOpenHelper +import net.zetetic.database.sqlcipher.SQLiteDatabase +import net.zetetic.database.sqlcipher.SQLiteOpenHelper -internal class CloudWatchDatabaseHelper(context: Context) : - SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) { +internal class CloudWatchDatabaseHelper(context: Context, databasePassphrase: String) : + SQLiteOpenHelper( + context, + DATABASE_NAME, + databasePassphrase, + null, + DATABASE_VERSION, + 0, + null, + null, + false + ) { companion object { internal const val DATABASE_NAME = "amplify.logging.cloudwatch.db" diff --git a/aws-logging-cloudwatch/src/main/java/com/amplifyframework/logging/cloudwatch/db/CloudWatchLoggingDatabase.kt b/aws-logging-cloudwatch/src/main/java/com/amplifyframework/logging/cloudwatch/db/CloudWatchLoggingDatabase.kt index 2d87e59f28..7749e7748f 100644 --- a/aws-logging-cloudwatch/src/main/java/com/amplifyframework/logging/cloudwatch/db/CloudWatchLoggingDatabase.kt +++ b/aws-logging-cloudwatch/src/main/java/com/amplifyframework/logging/cloudwatch/db/CloudWatchLoggingDatabase.kt @@ -26,7 +26,7 @@ import java.util.UUID import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import net.sqlcipher.database.SQLiteQueryBuilder +import net.zetetic.database.sqlcipher.SQLiteQueryBuilder internal class CloudWatchLoggingDatabase( private val context: Context, @@ -44,7 +44,7 @@ internal class CloudWatchLoggingDatabase( } private val database by lazy { System.loadLibrary("sqlcipher") - CloudWatchDatabaseHelper(context).getWritableDatabase(getDatabasePassphrase()) + CloudWatchDatabaseHelper(context, getDatabasePassphrase()).writableDatabase } private val basePath = "cloudwatchlogevents" private val contentUri: Uri diff --git a/aws-logging-cloudwatch/src/main/java/com/amplifyframework/logging/cloudwatch/db/LogEventTable.kt b/aws-logging-cloudwatch/src/main/java/com/amplifyframework/logging/cloudwatch/db/LogEventTable.kt index e582022093..655b4128d9 100644 --- a/aws-logging-cloudwatch/src/main/java/com/amplifyframework/logging/cloudwatch/db/LogEventTable.kt +++ b/aws-logging-cloudwatch/src/main/java/com/amplifyframework/logging/cloudwatch/db/LogEventTable.kt @@ -14,7 +14,7 @@ */ package com.amplifyframework.logging.cloudwatch.db -import net.sqlcipher.database.SQLiteDatabase +import net.zetetic.database.sqlcipher.SQLiteDatabase internal class LogEventTable { companion object { diff --git a/build.gradle.kts b/build.gradle.kts index 8f99b8f92a..4441e6d20d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -83,7 +83,7 @@ subprojects { allow("BSD-2-Clause") allow("CC0-1.0") allowUrl("https://developer.android.com/studio/terms.html") - allowDependency("net.zetetic", "android-database-sqlcipher", "4.5.4") { + allowDependency("net.zetetic", "sqlcipher-android", "4.6.1") { because("BSD style License") } allowDependency("org.jetbrains", "annotations", "16.0.1") { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2b07480351..87370d50a8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -47,7 +47,7 @@ okhttp = "5.0.0-alpha.11" robolectric = "4.7" rxjava = "3.0.6" slf4j = "2.0.6" -sqlcipher = "4.5.4" +sqlcipher = "4.6.1" tensorflow = "2.0.0" uuid = "4.0.1" totp = "1.0.1" @@ -102,7 +102,7 @@ oauth2 = { module = "com.google.auth:google-auth-library-oauth2-http", version.r okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } rxjava = { module = "io.reactivex.rxjava3:rxjava", version.ref = "rxjava" } slf4j = { module = "org.slf4j:slf4j-api", version.ref = "slf4j"} -sqlcipher= { module = "net.zetetic:android-database-sqlcipher", version.ref = "sqlcipher" } +sqlcipher= { module = "net.zetetic:sqlcipher-android", version.ref = "sqlcipher" } tensorflow = { module = "org.tensorflow:tensorflow-lite", version.ref="tensorflow" } uuidgen = { module = "com.fasterxml.uuid:java-uuid-generator", version.ref="uuid" } From 7581848c1877e0e1698c26f1ab375637a9ea72bd Mon Sep 17 00:00:00 2001 From: Vincent Tran Date: Wed, 15 Jan 2025 14:18:01 -0800 Subject: [PATCH 3/3] fix(storage): Fix SocketTimeoutException when executing a long multi-part upload (#2973) --- .../worker/AbortMultiPartUploadWorker.kt | 2 +- .../s3/transfer/worker/BaseTransferWorker.kt | 125 +-------------- .../transfer/worker/BlockingTransferWorker.kt | 100 ++++++++++++ .../worker/CompleteMultiPartUploadWorker.kt | 2 +- .../s3/transfer/worker/DownloadWorker.kt | 2 +- .../InitiateMultiPartUploadTransferWorker.kt | 4 +- .../worker/PartUploadTransferWorker.kt | 52 +++--- .../s3/transfer/worker/RouterWorker.kt | 2 +- .../transfer/worker/SinglePartUploadWorker.kt | 2 +- .../worker/SuspendingTransferWorker.kt | 150 ++++++++++++++++++ .../transfer/worker/TransferWorkerFactory.kt | 3 +- 11 files changed, 288 insertions(+), 156 deletions(-) create mode 100644 aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/transfer/worker/BlockingTransferWorker.kt create mode 100644 aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/transfer/worker/SuspendingTransferWorker.kt diff --git a/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/transfer/worker/AbortMultiPartUploadWorker.kt b/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/transfer/worker/AbortMultiPartUploadWorker.kt index 3f9cb5efb9..6fe337e33a 100644 --- a/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/transfer/worker/AbortMultiPartUploadWorker.kt +++ b/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/transfer/worker/AbortMultiPartUploadWorker.kt @@ -33,7 +33,7 @@ internal class AbortMultiPartUploadWorker( private val transferStatusUpdater: TransferStatusUpdater, context: Context, workerParameters: WorkerParameters -) : BaseTransferWorker(transferStatusUpdater, transferDB, context, workerParameters) { +) : SuspendingTransferWorker(transferStatusUpdater, transferDB, context, workerParameters) { override suspend fun performWork(): Result { val s3: S3Client = clientProvider.getStorageTransferClient(transferRecord.region, transferRecord.bucketName) diff --git a/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/transfer/worker/BaseTransferWorker.kt b/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/transfer/worker/BaseTransferWorker.kt index 1c3b24f6c4..119424477a 100644 --- a/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/transfer/worker/BaseTransferWorker.kt +++ b/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/transfer/worker/BaseTransferWorker.kt @@ -15,20 +15,10 @@ package com.amplifyframework.storage.s3.transfer.worker -import android.app.NotificationChannel -import android.app.NotificationManager import android.content.Context import android.net.ConnectivityManager import android.net.NetworkCapabilities import android.os.Build -import android.util.Log -import androidx.annotation.RequiresApi -import androidx.core.app.NotificationCompat -import androidx.work.CoroutineWorker -import androidx.work.Data -import androidx.work.ForegroundInfo -import androidx.work.WorkerParameters -import androidx.work.workDataOf import aws.sdk.kotlin.services.s3.model.ObjectCannedAcl import aws.sdk.kotlin.services.s3.model.PutObjectRequest import aws.sdk.kotlin.services.s3.model.RequestPayer @@ -37,40 +27,15 @@ import aws.sdk.kotlin.services.s3.model.StorageClass import aws.smithy.kotlin.runtime.content.ByteStream import aws.smithy.kotlin.runtime.content.fromFile import aws.smithy.kotlin.runtime.time.Instant -import com.amplifyframework.core.Amplify -import com.amplifyframework.core.category.CategoryType import com.amplifyframework.storage.ObjectMetadata -import com.amplifyframework.storage.TransferState -import com.amplifyframework.storage.s3.AWSS3StoragePlugin -import com.amplifyframework.storage.s3.R import com.amplifyframework.storage.s3.transfer.ProgressListener -import com.amplifyframework.storage.s3.transfer.TransferDB import com.amplifyframework.storage.s3.transfer.TransferRecord -import com.amplifyframework.storage.s3.transfer.TransferStatusUpdater import java.io.File -import java.lang.Exception -import java.net.SocketException -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.currentCoroutineContext -import kotlinx.coroutines.isActive /** * Base worker to perform transfer file task. */ -internal abstract class BaseTransferWorker( - private val transferStatusUpdater: TransferStatusUpdater, - private val transferDB: TransferDB, - context: Context, - workerParameters: WorkerParameters -) : CoroutineWorker(context, workerParameters) { - - internal lateinit var transferRecord: TransferRecord - internal lateinit var outputData: Data - private val logger = - Amplify.Logging.logger( - CategoryType.STORAGE, - AWSS3StoragePlugin.AWS_S3_STORAGE_LOG_NAMESPACE.format(this::class.java.simpleName) - ) +internal interface BaseTransferWorker { companion object { internal const val PART_RECORD_ID = "PART_RECORD_ID" @@ -86,91 +51,7 @@ internal abstract class BaseTransferWorker( internal const val MULTIPART_UPLOAD: String = "MULTIPART_UPLOAD" } - override suspend fun doWork(): Result { - // Foreground task is disabled until the foreground notification behavior and the recent customer feedback, - // it will be enabled in future based on the customer request. - val isForegroundTask: Boolean = (inputData.keyValueMap[RUN_AS_FOREGROUND_TASK] ?: false) as Boolean - if (isForegroundTask) { - setForegroundAsync(getForegroundInfo()) - } - val result = runCatching { - val transferRecordId = - inputData.keyValueMap[PART_RECORD_ID] as? Int ?: inputData.keyValueMap[TRANSFER_RECORD_ID] as Int - outputData = workDataOf(OUTPUT_TRANSFER_RECORD_ID to inputData.keyValueMap[TRANSFER_RECORD_ID] as Int) - transferDB.getTransferRecordById(transferRecordId)?.let { tr -> - transferRecord = tr - performWork() - } ?: return run { - Result.failure(outputData) - } - } - - return when { - result.isSuccess -> { - result.getOrThrow() - } - else -> { - val ex = result.exceptionOrNull() - if (currentCoroutineContext().isActive) { - logger.error("${this.javaClass.simpleName} failed with exception: ${Log.getStackTraceString(ex)}") - } - if (!currentCoroutineContext().isActive && isRetryableError(ex)) { - Result.retry() - } else { - transferStatusUpdater.updateOnError(transferRecord.id, Exception(ex)) - transferStatusUpdater.updateTransferState( - transferRecord.id, - TransferState.FAILED - ) - Result.failure(outputData) - } - } - } - } - - abstract suspend fun performWork(): Result - - internal open var maxRetryCount = 0 - - override suspend fun getForegroundInfo(): ForegroundInfo { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - createChannel() - } - val appIcon = R.drawable.amplify_storage_transfer_notification_icon - return ForegroundInfo( - 1, - NotificationCompat.Builder( - applicationContext, - applicationContext.getString(R.string.amplify_storage_notification_channel_id) - ) - .setSmallIcon(appIcon) - .setContentTitle(applicationContext.getString(R.string.amplify_storage_notification_title)) - .build() - ) - } - - private fun isRetryableError(e: Throwable?): Boolean { - return !isNetworkAvailable(applicationContext) || - runAttemptCount < maxRetryCount || - e is CancellationException || - // SocketException is thrown when download is terminated due to network disconnection. - e is SocketException - } - - @RequiresApi(Build.VERSION_CODES.O) - private fun createChannel() { - val notificationManager = - applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - notificationManager.createNotificationChannel( - NotificationChannel( - applicationContext.getString(R.string.amplify_storage_notification_channel_id), - applicationContext.getString(R.string.amplify_storage_notification_channel_name), - NotificationManager.IMPORTANCE_DEFAULT - ) - ) - } - - private fun isNetworkAvailable(context: Context): Boolean { + fun isNetworkAvailable(context: Context): Boolean { val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { @@ -198,7 +79,7 @@ internal abstract class BaseTransferWorker( return false } - internal fun createPutObjectRequest( + fun createPutObjectRequest( transferRecord: TransferRecord, progressListener: ProgressListener? ): PutObjectRequest { diff --git a/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/transfer/worker/BlockingTransferWorker.kt b/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/transfer/worker/BlockingTransferWorker.kt new file mode 100644 index 0000000000..b67b04a976 --- /dev/null +++ b/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/transfer/worker/BlockingTransferWorker.kt @@ -0,0 +1,100 @@ +/* + * 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.storage.s3.transfer.worker + +import android.content.Context +import android.util.Log +import androidx.work.Data +import androidx.work.Worker +import androidx.work.WorkerParameters +import androidx.work.workDataOf +import com.amplifyframework.core.Amplify +import com.amplifyframework.core.category.CategoryType +import com.amplifyframework.storage.TransferState +import com.amplifyframework.storage.s3.AWSS3StoragePlugin +import com.amplifyframework.storage.s3.transfer.TransferDB +import com.amplifyframework.storage.s3.transfer.TransferRecord +import com.amplifyframework.storage.s3.transfer.TransferStatusUpdater +import com.amplifyframework.storage.s3.transfer.worker.BaseTransferWorker.Companion.OUTPUT_TRANSFER_RECORD_ID +import com.amplifyframework.storage.s3.transfer.worker.BaseTransferWorker.Companion.PART_RECORD_ID +import com.amplifyframework.storage.s3.transfer.worker.BaseTransferWorker.Companion.TRANSFER_RECORD_ID +import java.lang.Exception +import java.net.SocketException + +/** + * Base worker to perform transfer file task. + */ +internal abstract class BlockingTransferWorker( + private val transferStatusUpdater: TransferStatusUpdater, + private val transferDB: TransferDB, + context: Context, + workerParameters: WorkerParameters +) : Worker(context, workerParameters), BaseTransferWorker { + + internal lateinit var transferRecord: TransferRecord + internal lateinit var outputData: Data + + private val logger = + Amplify.Logging.logger( + CategoryType.STORAGE, + AWSS3StoragePlugin.AWS_S3_STORAGE_LOG_NAMESPACE.format(this::class.java.simpleName) + ) + + override fun doWork(): Result { + val result = runCatching { + val transferRecordId = + inputData.keyValueMap[PART_RECORD_ID] as? Int ?: inputData.keyValueMap[TRANSFER_RECORD_ID] as Int + outputData = workDataOf(OUTPUT_TRANSFER_RECORD_ID to inputData.keyValueMap[TRANSFER_RECORD_ID] as Int) + transferDB.getTransferRecordById(transferRecordId)?.let { tr -> + transferRecord = tr + performWork() + } ?: return run { + Result.failure(outputData) + } + } + + return when { + result.isSuccess -> { + result.getOrThrow() + } + else -> { + val ex = result.exceptionOrNull() + logger.error("${this.javaClass.simpleName} failed with exception: ${Log.getStackTraceString(ex)}") + if (isRetryableError(ex)) { + Result.retry() + } else { + transferStatusUpdater.updateOnError(transferRecord.id, Exception(ex)) + transferStatusUpdater.updateTransferState( + transferRecord.id, + TransferState.FAILED + ) + Result.failure(outputData) + } + } + } + } + + abstract fun performWork(): Result + + internal open var maxRetryCount = 0 + + private fun isRetryableError(e: Throwable?): Boolean { + return !isNetworkAvailable(applicationContext) || + runAttemptCount < maxRetryCount || + // SocketException is thrown when download is terminated due to network disconnection. + e is SocketException + } +} diff --git a/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/transfer/worker/CompleteMultiPartUploadWorker.kt b/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/transfer/worker/CompleteMultiPartUploadWorker.kt index d7cadd5dd2..0c3f61e64d 100644 --- a/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/transfer/worker/CompleteMultiPartUploadWorker.kt +++ b/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/transfer/worker/CompleteMultiPartUploadWorker.kt @@ -33,7 +33,7 @@ internal class CompleteMultiPartUploadWorker( private val transferStatusUpdater: TransferStatusUpdater, context: Context, workerParameters: WorkerParameters -) : BaseTransferWorker(transferStatusUpdater, transferDB, context, workerParameters) { +) : SuspendingTransferWorker(transferStatusUpdater, transferDB, context, workerParameters) { override suspend fun performWork(): Result { val completedParts = transferDB.queryPartETagsOfUpload(transferRecord.id) diff --git a/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/transfer/worker/DownloadWorker.kt b/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/transfer/worker/DownloadWorker.kt index 9af6f8d70d..ac7ef9072a 100644 --- a/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/transfer/worker/DownloadWorker.kt +++ b/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/transfer/worker/DownloadWorker.kt @@ -46,7 +46,7 @@ internal class DownloadWorker( private val transferStatusUpdater: TransferStatusUpdater, context: Context, workerParameters: WorkerParameters -) : BaseTransferWorker(transferStatusUpdater, transferDB, context, workerParameters) { +) : SuspendingTransferWorker(transferStatusUpdater, transferDB, context, workerParameters) { private lateinit var downloadProgressListener: DownloadProgressListener private val defaultBufferSize = 8192L diff --git a/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/transfer/worker/InitiateMultiPartUploadTransferWorker.kt b/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/transfer/worker/InitiateMultiPartUploadTransferWorker.kt index b3c013b4bc..8251443b9c 100644 --- a/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/transfer/worker/InitiateMultiPartUploadTransferWorker.kt +++ b/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/transfer/worker/InitiateMultiPartUploadTransferWorker.kt @@ -24,6 +24,8 @@ import com.amplifyframework.storage.TransferState import com.amplifyframework.storage.s3.transfer.StorageTransferClientProvider import com.amplifyframework.storage.s3.transfer.TransferDB import com.amplifyframework.storage.s3.transfer.TransferStatusUpdater +import com.amplifyframework.storage.s3.transfer.worker.BaseTransferWorker.Companion.MULTI_PART_UPLOAD_ID +import com.amplifyframework.storage.s3.transfer.worker.BaseTransferWorker.Companion.TRANSFER_RECORD_ID /** * Worker to initiate multipart upload @@ -34,7 +36,7 @@ internal class InitiateMultiPartUploadTransferWorker( private val transferStatusUpdater: TransferStatusUpdater, context: Context, workerParameters: WorkerParameters -) : BaseTransferWorker(transferStatusUpdater, transferDB, context, workerParameters) { +) : SuspendingTransferWorker(transferStatusUpdater, transferDB, context, workerParameters) { override suspend fun performWork(): Result { val s3: S3Client = clientProvider.getStorageTransferClient(transferRecord.region, transferRecord.bucketName) diff --git a/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/transfer/worker/PartUploadTransferWorker.kt b/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/transfer/worker/PartUploadTransferWorker.kt index b7a0f6760d..106e27c97d 100644 --- a/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/transfer/worker/PartUploadTransferWorker.kt +++ b/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/transfer/worker/PartUploadTransferWorker.kt @@ -26,9 +26,9 @@ import com.amplifyframework.storage.s3.transfer.StorageTransferClientProvider import com.amplifyframework.storage.s3.transfer.TransferDB import com.amplifyframework.storage.s3.transfer.TransferStatusUpdater import com.amplifyframework.storage.s3.transfer.UploadProgressListenerInterceptor +import com.amplifyframework.storage.s3.transfer.worker.BaseTransferWorker.Companion.MULTI_PART_UPLOAD_ID import java.io.File -import kotlinx.coroutines.currentCoroutineContext -import kotlinx.coroutines.isActive +import kotlinx.coroutines.runBlocking /** * Worker to upload a part for multipart upload @@ -39,41 +39,39 @@ internal class PartUploadTransferWorker( private val transferStatusUpdater: TransferStatusUpdater, context: Context, workerParameters: WorkerParameters -) : BaseTransferWorker(transferStatusUpdater, transferDB, context, workerParameters) { +) : BlockingTransferWorker(transferStatusUpdater, transferDB, context, workerParameters) { private lateinit var multiPartUploadId: String private lateinit var partUploadProgressListener: PartUploadProgressListener override var maxRetryCount = 3 - override suspend fun performWork(): Result { - if (!currentCoroutineContext().isActive) { - return Result.retry() - } + override fun performWork(): Result { transferStatusUpdater.updateTransferState(transferRecord.mainUploadId, TransferState.IN_PROGRESS) multiPartUploadId = inputData.keyValueMap[MULTI_PART_UPLOAD_ID] as String partUploadProgressListener = PartUploadProgressListener(transferRecord, transferStatusUpdater) val s3: S3Client = clientProvider.getStorageTransferClient(transferRecord.region, transferRecord.bucketName) - return s3.withConfig { - interceptors += UploadProgressListenerInterceptor(partUploadProgressListener) - enableAccelerate = transferRecord.useAccelerateEndpoint == 1 - }.uploadPart { - bucket = transferRecord.bucketName - key = transferRecord.key - uploadId = multiPartUploadId - body = File(transferRecord.file).asByteStream( - start = transferRecord.fileOffset, - transferRecord.fileOffset + transferRecord.bytesTotal - 1 - ) - partNumber = transferRecord.partNumber - }.let { response -> - response.eTag?.let { tag -> - transferDB.updateETag(transferRecord.id, tag) - transferDB.updateState(transferRecord.id, TransferState.PART_COMPLETED) - updateProgress() - Result.success(outputData) - } ?: run { - throw IllegalStateException("Etag is empty") + + return runBlocking { + s3.withConfig { + interceptors += UploadProgressListenerInterceptor(partUploadProgressListener) + enableAccelerate = transferRecord.useAccelerateEndpoint == 1 + }.uploadPart { + bucket = transferRecord.bucketName + key = transferRecord.key + uploadId = multiPartUploadId + body = File(transferRecord.file).asByteStream( + start = transferRecord.fileOffset, + transferRecord.fileOffset + transferRecord.bytesTotal - 1 + ) + partNumber = transferRecord.partNumber } + }.eTag?.let { tag -> + transferDB.updateETag(transferRecord.id, tag) + transferDB.updateState(transferRecord.id, TransferState.PART_COMPLETED) + updateProgress() + return Result.success(outputData) + } ?: run { + throw IllegalStateException("Etag is empty") } } diff --git a/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/transfer/worker/RouterWorker.kt b/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/transfer/worker/RouterWorker.kt index b589e60a92..e491ce1308 100644 --- a/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/transfer/worker/RouterWorker.kt +++ b/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/transfer/worker/RouterWorker.kt @@ -43,7 +43,7 @@ internal class RouterWorker( ?: throw IllegalArgumentException("Worker class name is missing") private val workerId = parameter.inputData.getString(BaseTransferWorker.WORKER_ID) - private var delegateWorker: BaseTransferWorker? = null + private var delegateWorker: ListenableWorker? = null companion object { internal const val WORKER_CLASS_NAME = "WORKER_CLASS_NAME" diff --git a/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/transfer/worker/SinglePartUploadWorker.kt b/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/transfer/worker/SinglePartUploadWorker.kt index 515c36befc..2502422b4d 100644 --- a/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/transfer/worker/SinglePartUploadWorker.kt +++ b/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/transfer/worker/SinglePartUploadWorker.kt @@ -34,7 +34,7 @@ internal class SinglePartUploadWorker( private val transferStatusUpdater: TransferStatusUpdater, context: Context, workerParameters: WorkerParameters -) : BaseTransferWorker(transferStatusUpdater, transferDB, context, workerParameters) { +) : SuspendingTransferWorker(transferStatusUpdater, transferDB, context, workerParameters) { private lateinit var uploadProgressListener: UploadProgressListener diff --git a/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/transfer/worker/SuspendingTransferWorker.kt b/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/transfer/worker/SuspendingTransferWorker.kt new file mode 100644 index 0000000000..8621d80987 --- /dev/null +++ b/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/transfer/worker/SuspendingTransferWorker.kt @@ -0,0 +1,150 @@ +/* + * 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.storage.s3.transfer.worker + +import android.app.NotificationChannel +import android.app.NotificationManager +import android.content.Context +import android.os.Build +import android.util.Log +import androidx.annotation.RequiresApi +import androidx.core.app.NotificationCompat +import androidx.work.CoroutineWorker +import androidx.work.Data +import androidx.work.ForegroundInfo +import androidx.work.WorkerParameters +import androidx.work.workDataOf +import com.amplifyframework.core.Amplify +import com.amplifyframework.core.category.CategoryType +import com.amplifyframework.storage.TransferState +import com.amplifyframework.storage.s3.AWSS3StoragePlugin +import com.amplifyframework.storage.s3.R +import com.amplifyframework.storage.s3.transfer.TransferDB +import com.amplifyframework.storage.s3.transfer.TransferRecord +import com.amplifyframework.storage.s3.transfer.TransferStatusUpdater +import com.amplifyframework.storage.s3.transfer.worker.BaseTransferWorker.Companion.OUTPUT_TRANSFER_RECORD_ID +import com.amplifyframework.storage.s3.transfer.worker.BaseTransferWorker.Companion.PART_RECORD_ID +import com.amplifyframework.storage.s3.transfer.worker.BaseTransferWorker.Companion.RUN_AS_FOREGROUND_TASK +import com.amplifyframework.storage.s3.transfer.worker.BaseTransferWorker.Companion.TRANSFER_RECORD_ID +import java.lang.Exception +import java.net.SocketException +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.isActive + +/** + * Base worker to perform transfer file task. + */ +internal abstract class SuspendingTransferWorker( + private val transferStatusUpdater: TransferStatusUpdater, + private val transferDB: TransferDB, + context: Context, + workerParameters: WorkerParameters +) : CoroutineWorker(context, workerParameters), BaseTransferWorker { + + internal lateinit var transferRecord: TransferRecord + internal lateinit var outputData: Data + + private val logger = + Amplify.Logging.logger( + CategoryType.STORAGE, + AWSS3StoragePlugin.AWS_S3_STORAGE_LOG_NAMESPACE.format(this::class.java.simpleName) + ) + + override suspend fun doWork(): Result { + // Foreground task is disabled until the foreground notification behavior and the recent customer feedback, + // it will be enabled in future based on the customer request. + val isForegroundTask: Boolean = (inputData.keyValueMap[RUN_AS_FOREGROUND_TASK] ?: false) as Boolean + if (isForegroundTask) { + setForegroundAsync(getForegroundInfo()) + } + val result = runCatching { + val transferRecordId = + inputData.keyValueMap[PART_RECORD_ID] as? Int ?: inputData.keyValueMap[TRANSFER_RECORD_ID] as Int + outputData = workDataOf(OUTPUT_TRANSFER_RECORD_ID to inputData.keyValueMap[TRANSFER_RECORD_ID] as Int) + transferDB.getTransferRecordById(transferRecordId)?.let { tr -> + transferRecord = tr + performWork() + } ?: return run { + Result.failure(outputData) + } + } + + return when { + result.isSuccess -> { + result.getOrThrow() + } + else -> { + val ex = result.exceptionOrNull() + if (currentCoroutineContext().isActive) { + logger.error("${this.javaClass.simpleName} failed with exception: ${Log.getStackTraceString(ex)}") + } + if (!currentCoroutineContext().isActive && isRetryableError(ex)) { + Result.retry() + } else { + transferStatusUpdater.updateOnError(transferRecord.id, Exception(ex)) + transferStatusUpdater.updateTransferState( + transferRecord.id, + TransferState.FAILED + ) + Result.failure(outputData) + } + } + } + } + + abstract suspend fun performWork(): Result + + internal open var maxRetryCount = 0 + + override suspend fun getForegroundInfo(): ForegroundInfo { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + createChannel() + } + val appIcon = R.drawable.amplify_storage_transfer_notification_icon + return ForegroundInfo( + 1, + NotificationCompat.Builder( + applicationContext, + applicationContext.getString(R.string.amplify_storage_notification_channel_id) + ) + .setSmallIcon(appIcon) + .setContentTitle(applicationContext.getString(R.string.amplify_storage_notification_title)) + .build() + ) + } + + private fun isRetryableError(e: Throwable?): Boolean { + return !isNetworkAvailable(applicationContext) || + runAttemptCount < maxRetryCount || + e is CancellationException || + // SocketException is thrown when download is terminated due to network disconnection. + e is SocketException + } + + @RequiresApi(Build.VERSION_CODES.O) + private fun createChannel() { + val notificationManager = + applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager.createNotificationChannel( + NotificationChannel( + applicationContext.getString(R.string.amplify_storage_notification_channel_id), + applicationContext.getString(R.string.amplify_storage_notification_channel_name), + NotificationManager.IMPORTANCE_DEFAULT + ) + ) + } +} diff --git a/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/transfer/worker/TransferWorkerFactory.kt b/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/transfer/worker/TransferWorkerFactory.kt index d814473643..4fa627ba6b 100644 --- a/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/transfer/worker/TransferWorkerFactory.kt +++ b/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/transfer/worker/TransferWorkerFactory.kt @@ -15,6 +15,7 @@ package com.amplifyframework.storage.s3.transfer.worker import android.content.Context +import androidx.work.ListenableWorker import androidx.work.WorkerFactory import androidx.work.WorkerParameters import com.amplifyframework.storage.s3.transfer.StorageTransferClientProvider @@ -33,7 +34,7 @@ internal class TransferWorkerFactory( appContext: Context, workerClassName: String, workerParameters: WorkerParameters - ): BaseTransferWorker { + ): ListenableWorker { when (workerClassName) { DownloadWorker::class.java.name -> return DownloadWorker(