From 1806b58ba4ba160bf30e9288b4ee37b6a9591b8f Mon Sep 17 00:00:00 2001 From: Rajat <143978428+mrajatttt@users.noreply.github.com> Date: Mon, 28 Oct 2024 16:36:09 -0700 Subject: [PATCH] Adding support for non-hilt Applications (#37) * Adding support for non-hilt Applications * Update README.md --- README.md | 3 +- chat-sdk/build.gradle.kts | 25 ++++ .../amazon/connect/chat/sdk/di/ChatModule.kt | 13 +- .../connect/chat/sdk/di/NetworkModule.kt | 44 ++---- .../connect/chat/sdk/network/AWSClient.kt | 9 ++ .../sdk/network/NetworkConnectionManager.kt | 2 +- .../sdk/network/RetrofitServiceCreator.kt | 28 ++++ .../chat/sdk/provider/ChatSessionProvider.kt | 136 +++++++++++++++--- chat-sdk/version.properties | 2 +- gradle/libs.versions.toml | 2 +- 10 files changed, 207 insertions(+), 57 deletions(-) create mode 100644 chat-sdk/src/main/java/com/amazon/connect/chat/sdk/network/RetrofitServiceCreator.kt diff --git a/README.md b/README.md index eb13aff..cac6fca 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ dependencies { ## Getting Started -The first step to leveraging the Amazon Connect Chat SDK after installation is to import the library into your file. Next, let's call the StartChatContact API and pass the response details into the SDK’s ChatSession object. Here is an [example](TODO - Add link to UI Example) of how we would set this up in Kotlin. For reference, you can visit the [AndroidChatExample demo](https://github.com/amazon-connect/amazon-connect-chat-ui-examples/tree/master/mobileChatExamples/androidChatExample) within the [Amazon Connect Chat UI Examples](https://github.com/amazon-connect/amazon-connect-chat-ui-examples/tree/master) GitHub repository. +The first step to leveraging the Amazon Connect Chat SDK after installation is to import the library into your file. Next, let's call the StartChatContact API and pass the response details into the SDK’s ChatSession object. For reference, you can visit the [AndroidChatExample demo](https://github.com/amazon-connect/amazon-connect-chat-ui-examples/tree/master/mobileChatExamples/androidChatExample) within the [Amazon Connect Chat UI Examples](https://github.com/amazon-connect/amazon-connect-chat-ui-examples/tree/master) GitHub repository. The majority of the SDKs functionality will be accessed through the `ChatSession` object. In order to use this object in the file, you can inject it using `@HiltViewModel`: @@ -70,6 +70,7 @@ If you are not using Hilt, then you can initialise `ChatSession` like this: ``` private val chatSession = ChatSessionProvider.getChatSession(context) ``` +> Note: When not using Hilt/Dagger for dependency injection, you may need to manually import the necessary dependencies for [AWSConnectParticipant](https://github.com/aws-amplify/aws-sdk-android) and [AWSCore](https://github.com/aws-amplify/aws-sdk-android) in your app’s build.gradle file. With Hilt/Dagger, these dependencies are managed automatically, but for manual implementations, you must include them explicitly to ensure all required classes are available. In this example, we are using a `ChatViewModel` class that helps bridge UI and SDK communication. This class is responsible for managing interactions with the SDK's ChatSession object. From here, we can access the SDK's suite of APIs from the `chatSession` property. diff --git a/chat-sdk/build.gradle.kts b/chat-sdk/build.gradle.kts index 1800d13..1c506fb 100644 --- a/chat-sdk/build.gradle.kts +++ b/chat-sdk/build.gradle.kts @@ -129,6 +129,31 @@ tasks.withType().configureEach { dependsOn(tasks.named("assembleRelease")) } +// For local publishing +// Can be used in example app like below +// Keeping group Id different for local testing purpose +// implementation("com.amazon.connect.chat.sdk:connect-chat-sdk:1.0.0") +publishing { + publications { + // Create a MavenPublication for the release build type + create("release") { + afterEvaluate { + artifact(tasks.getByName("bundleReleaseAar")) + } + groupId = "com.amazon.connect.chat.sdk" + artifactId = "connect-chat-sdk" + version = "1.0.0" + + + } + } + // Define the repository where the artifact will be published + repositories { + mavenLocal() + } +} + + // Test summary gradle file apply(from = "test-summary.gradle.kts") diff --git a/chat-sdk/src/main/java/com/amazon/connect/chat/sdk/di/ChatModule.kt b/chat-sdk/src/main/java/com/amazon/connect/chat/sdk/di/ChatModule.kt index eb569db..5706214 100644 --- a/chat-sdk/src/main/java/com/amazon/connect/chat/sdk/di/ChatModule.kt +++ b/chat-sdk/src/main/java/com/amazon/connect/chat/sdk/di/ChatModule.kt @@ -19,6 +19,7 @@ import com.amazon.connect.chat.sdk.provider.ConnectionDetailsProvider import com.amazon.connect.chat.sdk.provider.ConnectionDetailsProviderImpl import dagger.Module import dagger.Provides +import dagger.hilt.EntryPoint import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent @@ -48,7 +49,17 @@ object ChatModule { attachmentsManager: AttachmentsManager, messageReceiptsManager: MessageReceiptsManager, ): ChatService { - return ChatServiceImpl(context, awsClient, connectionDetailsProvider, webSocketManager, metricsManager, attachmentsManager, messageReceiptsManager) + return ChatServiceImpl(context, awsClient, connectionDetailsProvider, + webSocketManager, metricsManager, attachmentsManager, messageReceiptsManager) + } + + /** + * Entry point for Hilt to provide ChatSession. + */ + @EntryPoint + @InstallIn(SingletonComponent::class) + interface ChatSessionEntryPoint { + fun getChatSession(): ChatSession } /** diff --git a/chat-sdk/src/main/java/com/amazon/connect/chat/sdk/di/NetworkModule.kt b/chat-sdk/src/main/java/com/amazon/connect/chat/sdk/di/NetworkModule.kt index 778bdde..0fa5cd6 100644 --- a/chat-sdk/src/main/java/com/amazon/connect/chat/sdk/di/NetworkModule.kt +++ b/chat-sdk/src/main/java/com/amazon/connect/chat/sdk/di/NetworkModule.kt @@ -4,17 +4,17 @@ package com.amazon.connect.chat.sdk.di import android.content.Context -import com.amazon.connect.chat.sdk.network.api.APIClient import com.amazon.connect.chat.sdk.network.AWSClient import com.amazon.connect.chat.sdk.network.AWSClientImpl -import com.amazon.connect.chat.sdk.network.api.ApiUrl +import com.amazon.connect.chat.sdk.network.NetworkConnectionManager +import com.amazon.connect.chat.sdk.network.RetrofitServiceCreator +import com.amazon.connect.chat.sdk.network.api.APIClient import com.amazon.connect.chat.sdk.network.api.AttachmentsInterface import com.amazon.connect.chat.sdk.network.api.MetricsInterface -import com.amazon.connect.chat.sdk.repository.MetricsManager -import com.amazon.connect.chat.sdk.network.NetworkConnectionManager import com.amazon.connect.chat.sdk.repository.AttachmentsManager import com.amazon.connect.chat.sdk.repository.MessageReceiptsManager import com.amazon.connect.chat.sdk.repository.MessageReceiptsManagerImpl +import com.amazon.connect.chat.sdk.repository.MetricsManager import com.amazon.connect.chat.sdk.utils.MetricsUtils.getMetricsEndpoint import com.amazonaws.services.connectparticipant.AmazonConnectParticipantClient import dagger.Module @@ -31,8 +31,6 @@ import javax.inject.Singleton @InstallIn(SingletonComponent::class) object NetworkModule { - private const val defaultApiUrl = "https://www.example.com/v1/" - /** * Provides a singleton instance of OkHttpClient. * @@ -67,14 +65,14 @@ object NetworkModule { @Provides @Singleton fun provideMetricsInterface(retrofitBuilder: Retrofit.Builder): MetricsInterface { - return createService(MetricsInterface::class.java, retrofitBuilder, url=getMetricsEndpoint()) + return RetrofitServiceCreator.createService(MetricsInterface::class.java, retrofitBuilder, url = getMetricsEndpoint()) } /** * Provides a singleton instance of MetricsInterface. * - * @param retrofitBuilder The Retrofit.Builder instance for creating the service. - * @return An instance of MetricsInterface. + * @param apiClient The APIClient instance for API operations. + * @return An instance of MetricsManager. */ @Provides @Singleton @@ -83,10 +81,11 @@ object NetworkModule { } /** - * Provides a singleton instance of MetricsInterface. - * - * @param retrofitBuilder The Retrofit.Builder instance for creating the service. - * @return An instance of MetricsInterface. + * Provides a singleton instance of AttachmentsManager. + * @param context The application context. + * @param awsClient The AWSClient instance for AWS SDK calls. + * @param apiClient The APIClient instance for API operations. + * @return An instance of AttachmentsManager. */ @Provides @Singleton @@ -103,7 +102,7 @@ object NetworkModule { @Provides @Singleton fun provideAttachmentsInterface(retrofitBuilder: Retrofit.Builder): AttachmentsInterface { - return createService(AttachmentsInterface::class.java, retrofitBuilder) + return RetrofitServiceCreator.createService(AttachmentsInterface::class.java, retrofitBuilder) } /** @@ -163,21 +162,4 @@ object NetworkModule { fun provideMessageReceiptsManager(): MessageReceiptsManager { return MessageReceiptsManagerImpl() } - - /** - * Creates a Retrofit service for the specified class. - * - * @param T The type of the service. - * @param clazz The class of the service. - * @param retrofitBuilder The Retrofit.Builder instance for creating the service. - * @return An instance of the specified service class. - */ - private fun createService(clazz: Class, retrofitBuilder: Retrofit.Builder, url: String? = null): T { - // Check if the service has an annotation - val apiUrlAnnotation = clazz.annotations.find { it is ApiUrl } as ApiUrl? - // Take the URL value, otherwise use the default - val apiUrl = url ?: (apiUrlAnnotation?.url ?: defaultApiUrl) - // Create the service using the extracted URL - return retrofitBuilder.baseUrl(apiUrl).build().create(clazz) - } } \ No newline at end of file diff --git a/chat-sdk/src/main/java/com/amazon/connect/chat/sdk/network/AWSClient.kt b/chat-sdk/src/main/java/com/amazon/connect/chat/sdk/network/AWSClient.kt index 2b7a0c7..5b79df0 100644 --- a/chat-sdk/src/main/java/com/amazon/connect/chat/sdk/network/AWSClient.kt +++ b/chat-sdk/src/main/java/com/amazon/connect/chat/sdk/network/AWSClient.kt @@ -108,6 +108,15 @@ class AWSClientImpl @Inject constructor( private val connectParticipantClient: AmazonConnectParticipantClient ) : AWSClient { + companion object { + fun create(): AWSClient { + // Create an AmazonConnectParticipantClient + val connectParticipantClient = AmazonConnectParticipantClient() + + return AWSClientImpl(connectParticipantClient) + } + } + override fun configure(config: GlobalConfig) { connectParticipantClient.setRegion(Region.getRegion(config.region)) } diff --git a/chat-sdk/src/main/java/com/amazon/connect/chat/sdk/network/NetworkConnectionManager.kt b/chat-sdk/src/main/java/com/amazon/connect/chat/sdk/network/NetworkConnectionManager.kt index 6b7dd75..c788a88 100644 --- a/chat-sdk/src/main/java/com/amazon/connect/chat/sdk/network/NetworkConnectionManager.kt +++ b/chat-sdk/src/main/java/com/amazon/connect/chat/sdk/network/NetworkConnectionManager.kt @@ -11,7 +11,7 @@ import android.net.NetworkRequest import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -class NetworkConnectionManager private constructor(context: Context) { +class NetworkConnectionManager(context: Context) { private val connectivityManager: ConnectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager diff --git a/chat-sdk/src/main/java/com/amazon/connect/chat/sdk/network/RetrofitServiceCreator.kt b/chat-sdk/src/main/java/com/amazon/connect/chat/sdk/network/RetrofitServiceCreator.kt new file mode 100644 index 0000000..4ed1b51 --- /dev/null +++ b/chat-sdk/src/main/java/com/amazon/connect/chat/sdk/network/RetrofitServiceCreator.kt @@ -0,0 +1,28 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.amazon.connect.chat.sdk.network + +import com.amazon.connect.chat.sdk.network.api.ApiUrl +import retrofit2.Retrofit + +object RetrofitServiceCreator { + private const val defaultApiUrl = "https://www.example.com/v1/" + + /** + * Creates a Retrofit service for the specified class. + * + * @param T The type of the service. + * @param clazz The class of the service. + * @param retrofitBuilder The Retrofit.Builder instance for creating the service. + * @return An instance of the specified service class. + */ + fun createService(clazz: Class, retrofitBuilder: Retrofit.Builder, url: String? = null): T { + // Check if the service has an @ApiUrl annotation + val apiUrlAnnotation = clazz.annotations.find { it is ApiUrl } as ApiUrl? + // Take the URL value, otherwise use the default + val apiUrl = url ?: (apiUrlAnnotation?.url ?: defaultApiUrl) + // Create the service using the extracted URL + return retrofitBuilder.baseUrl(apiUrl).build().create(clazz) + } +} diff --git a/chat-sdk/src/main/java/com/amazon/connect/chat/sdk/provider/ChatSessionProvider.kt b/chat-sdk/src/main/java/com/amazon/connect/chat/sdk/provider/ChatSessionProvider.kt index 81cae56..fdb7e50 100644 --- a/chat-sdk/src/main/java/com/amazon/connect/chat/sdk/provider/ChatSessionProvider.kt +++ b/chat-sdk/src/main/java/com/amazon/connect/chat/sdk/provider/ChatSessionProvider.kt @@ -1,33 +1,127 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.amazon.connect.chat.sdk.provider - import android.content.Context import com.amazon.connect.chat.sdk.ChatSession -import dagger.hilt.EntryPoint -import dagger.hilt.InstallIn +import com.amazon.connect.chat.sdk.ChatSessionImpl +import com.amazon.connect.chat.sdk.di.ChatModule +import com.amazon.connect.chat.sdk.network.AWSClientImpl +import com.amazon.connect.chat.sdk.network.NetworkConnectionManager +import com.amazon.connect.chat.sdk.network.RetrofitServiceCreator.createService +import com.amazon.connect.chat.sdk.network.WebSocketManagerImpl +import com.amazon.connect.chat.sdk.network.api.APIClient +import com.amazon.connect.chat.sdk.network.api.AttachmentsInterface +import com.amazon.connect.chat.sdk.network.api.MetricsInterface +import com.amazon.connect.chat.sdk.provider.ConnectionDetailsProviderImpl +import com.amazon.connect.chat.sdk.repository.AttachmentsManager +import com.amazon.connect.chat.sdk.repository.ChatServiceImpl +import com.amazon.connect.chat.sdk.repository.MessageReceiptsManagerImpl +import com.amazon.connect.chat.sdk.repository.MetricsManager +import com.amazon.connect.chat.sdk.utils.MetricsUtils.getMetricsEndpoint +import com.amazon.connect.chat.sdk.utils.logger.SDKLogger import dagger.hilt.android.EntryPointAccessors -import dagger.hilt.components.SingletonComponent - -@EntryPoint -@InstallIn(SingletonComponent::class) -interface ChatSessionEntryPoint { - fun getChatSession(): ChatSession -} +import kotlinx.coroutines.Dispatchers +import okhttp3.OkHttpClient +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory object ChatSessionProvider { + @Volatile private var chatSession: ChatSession? = null + // Public method for customers to get ChatSession fun getChatSession(context: Context): ChatSession { - if (chatSession == null) { - val appContext = context.applicationContext + return chatSession ?: synchronized(this) { + chatSession ?: initializeChatSession(context).also { + chatSession = it + } + } + } + + // Private method to initialize ChatSession using Hilt or manual fallback + private fun initializeChatSession(context: Context): ChatSession { + return if (isHiltAvailable()) { + // Use Hilt's EntryPoint mechanism if Hilt is available val entryPoint = EntryPointAccessors.fromApplication( - appContext, - ChatSessionEntryPoint::class.java + context.applicationContext, + ChatModule.ChatSessionEntryPoint::class.java ) - chatSession = entryPoint.getChatSession() + entryPoint.getChatSession() + } else { + // Fallback to manual initialization + createChatSessionManually(context) } - return chatSession!! } -} \ No newline at end of file + + // Method to check if Hilt is available + private fun isHiltAvailable(): Boolean { + return try { + Class.forName("dagger.hilt.EntryPoints") + true + } catch (e: ClassNotFoundException) { + SDKLogger.logger.logDebug {"Hilt is not available"} + false + } + } + + // Manual initialization of ChatSession for non-Hilt users + private fun createChatSessionManually(context: Context): ChatSession { + val appContext = context.applicationContext + + // Step 1: Create AWS Client + val awsClient = AWSClientImpl.create() + + // Step 2: Create Network Connection Manager + val networkConnectionManager = NetworkConnectionManager(appContext) + + // Step 3: Create WebSocket Manager + val webSocketManager = WebSocketManagerImpl( + dispatcher = Dispatchers.IO, + networkConnectionManager = networkConnectionManager + ) + + // Step 4: Create Retrofit Builder + val retrofitBuilder = createRetrofitBuilder() + + // Step 5: Create API Client + val apiClient = createAPIClient(retrofitBuilder) + + // Step 6: Create Other Dependencies + val metricsManager = MetricsManager(apiClient) + val attachmentsManager = AttachmentsManager(appContext, awsClient, apiClient) + val messageReceiptsManager = MessageReceiptsManagerImpl() + val connectionDetailsProvider = ConnectionDetailsProviderImpl() + + // Step 7: Create ChatService and return ChatSessionImpl + val chatService = ChatServiceImpl( + appContext, + awsClient, + connectionDetailsProvider, + webSocketManager, + metricsManager, + attachmentsManager, + messageReceiptsManager + ) + + return ChatSessionImpl(chatService) + } + + // Helper method to create Retrofit Builder + private fun createRetrofitBuilder(): Retrofit.Builder { + val okHttpClient = OkHttpClient.Builder().build() + return Retrofit.Builder() + .client(okHttpClient) + .addConverterFactory(GsonConverterFactory.create()) + } + + // Helper method to create APIClient + private fun createAPIClient(retrofitBuilder: Retrofit.Builder): APIClient { + val metricsInterface: MetricsInterface = createService( + MetricsInterface::class.java, + retrofitBuilder, + url = getMetricsEndpoint() + ) + val attachmentsInterface: AttachmentsInterface = createService( + AttachmentsInterface::class.java, + retrofitBuilder + ) + return APIClient(metricsInterface, attachmentsInterface) + } +} diff --git a/chat-sdk/version.properties b/chat-sdk/version.properties index 8e6cfd5..e6a04ed 100644 --- a/chat-sdk/version.properties +++ b/chat-sdk/version.properties @@ -1,3 +1,3 @@ -sdkVersion=0.0.1-alpha +sdkVersion=1.0.1 groupId=software.aws.connect artifactId=amazon-connect-chat-android diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2326b5c..0d94353 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -84,7 +84,7 @@ awsSdkConnectParticipant = { module = "com.amazonaws:aws-android-sdk-connectpart robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" } serializationJson = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serializationJson" } coilCompose = { module = "io.coil-kt:coil-compose", version.ref = "coilCompose" } -chatSdk = { module = "com.amazon.connect.chat:library", version.ref = "chatSdk" } +chatSdk = { module = "software.aws.connect:amazon-connect-chat-android", version.ref = "chatSdk" } runtimeLivedata = { group = "androidx.compose.runtime", name = "runtime-livedata", version.ref = "runtimeLivedata" } appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } material = { group = "com.google.android.material", name = "material", version.ref = "material" }