diff --git a/Android/AmazonConnectInAppCallingAndroidSample/README.md b/Android/AmazonConnectInAppCallingAndroidSample/README.md index 2abd9a4..2baad82 100644 --- a/Android/AmazonConnectInAppCallingAndroidSample/README.md +++ b/Android/AmazonConnectInAppCallingAndroidSample/README.md @@ -1,36 +1,75 @@ -# Amazon Connect In-App Calling Sample - Android - -This sample Android app demonstrates how to interact with Amazon Connect APIs to start an in-app call -with Amazon Connect and send DTMF. - -This sample app works together with the serverless solution deployed -by the `Amazon Connect In-App Calling API Sample` -to deliver an in-app calling experience. Please make sure you follow the `README` and deploy the serverless demo first, and note down the output. - -> NOTE: this sample is for demo purposes. - -## Setup -1. `Amazon Connect In-App Calling API Sample` has been deployed. -2. Git clone the repo -3. Open the project from [Android Studio](https://developer.android.com/studio) -4. Make sure Android SDK location has been properly configured by clicking *File* -> *Project Structure* -> *SDK Location*, or go to `/local.properties`, there should be `sdk.dir` property -5. Connect a physical Android device, make sure developer mode is on by following [this guide](https://developer.android.com/studio/debug/dev-options) - 1. Fill in required configurations in `CallConfiguration.kt` - ```kotlin - data class CallConfiguration( - // ... - val connectInstanceId: String = "", // your Amazon Connect instance Id - val contactFlowId: String = "", // your contact flow Id that you want to associated with the calls - val startWebrtcEndpoint: String = "", // the endpoint URL of startWebrtcContact API deployed by the serverless demo - val createParticipantConnectionEndpoint: String = "", // the endpoint URL of createParticipantConnection API deployed by the serverless demo - val sendMessageEndpoint: String = "", // the endpoint URL of sendMessage API deployed by the serverless demo - // ... - ) - ``` -6. In Android Studio, choose your connected device, and target `app`, click run - -### Key files and methods in the sample code - -**CallManager**: Contains methods to: 1) manage the call session, 2) send DTMF 3) audio / video controls (e.g., mute, unmute, start, stop video) 4) handle SDK meeting events (e.g., meeting started / ende, attendee joined, dropped) - -**Call Sheet**: Main UI for hosting the call session +# Amazon Connect In-App Calling Sample – Android + +This sample Android app demonstrates how to interact with Amazon Connect APIs to start an **in-app voice/video call** and send **DTMF**. It also supports **screen sharing** via Amazon Chime SDK. + +This app works together with the serverless solution provided by the [`Amazon Connect In-App Calling API Sample`](https://github.com/amazon-connect/amazon-connect-in-app-calling-examples/tree/main/Backend/AmazonConnectNetraApiSample). +➡️ **Please deploy the backend first and take note of the endpoint outputs.** + + +--- + +## 📱 Preview + +App Screenshot + +--- + +## 🚀 Setup + +1. **Deploy the Backend** + Follow the [Amazon Connect In-App Calling API Sample](https://github.com/amazon-connect/amazon-connect-in-app-calling-examples/tree/main/Backend/AmazonConnectNetraApiSample) README to deploy the Lambda-based APIs via AWS CDK. + +2. **Clone this repo** + + ```bash + git clone https://github.com/amazon-connect/amazon-connect-in-app-calling-examples.git + cd Android/AmazonConnectInAppCallingAndroidSample + ``` + +3. **Configure Android SDK** + - Open in [Android Studio](https://developer.android.com/studio) + - Ensure Android SDK path is set: + - File → Project Structure → SDK Location + - Or edit local.properties: + ```properties + sdk.dir=/your/android/sdk/path + ``` + +4. **Provide Required Configuration** + Add the following entries in your local.properties file: + ```properties + connect.instanceId=your-connect-instance-id + connect.contactFlowId=your-contact-flow-id + endpoints.startWebRTC=https://your-start-webrtc-endpoint/ + endpoints.createParticipant=https://your-create-participant-endpoint/ + endpoints.sendMessage=https://your-send-message-endpoint/ + ``` + These values are used as default and can be changed in the app’s Configuration dialog at runtime. + +5. **Run the App** + - Connect a physical Android device + - Hit ▶️ Run in Android Studio and target the app + +6. **Start a Call** + - Enter your Display Name and City + - Tap Start Call to initiate + - Click **Config** button to update endpoints if needed + +## 🧩 Key Components + +| Component | File / Class Name | Responsibility | +|------------------------|----------------------------------------|-------------------------------------------------------------------------------| +| Call Sheet | `CallSheet.kt` | Main UI sheet that shows during an active call | +| Call Manager | `CallManager.kt` | Manages the call session, audio/video state, DTMF, and participants events | +| Call Connection | `CallConnection.kt` | Bridges the in-app call with Android Telecom. Manages call audio routing changes (e.g. Bluetooth, speaker), and handles system-level disconnects or conflicts with other ongoing calls. | +| Screen Share Manager | `ScreenShareManager.kt` | Manages screen capture and sharing | +| Configuration Repository | `ConfigRepository.kt` | Central source for retrieving call-related configuration used to start calls | + +## ✅ Features +- In-app audio/video calling with Amazon Connect +- Dynamic contact attributes (e.g., name and city) +- DTMF (dual-tone signaling) +- Screen sharing +- Video background blur +- Voice focus + diff --git a/Android/AmazonConnectInAppCallingAndroidSample/app/build.gradle b/Android/AmazonConnectInAppCallingAndroidSample/app/build.gradle index 92dc58e..46fd1d5 100644 --- a/Android/AmazonConnectInAppCallingAndroidSample/app/build.gradle +++ b/Android/AmazonConnectInAppCallingAndroidSample/app/build.gradle @@ -8,12 +8,12 @@ localProperties.load(new FileInputStream((rootProject.file("local.properties"))) android { namespace 'com.amazonaws.services.connect.inappcalling.sample' - compileSdk 33 + compileSdk 35 defaultConfig { applicationId "com.amazonaws.services.connect.inappcalling.sample" minSdk 24 - targetSdk 33 + targetSdk 35 versionCode 1 versionName "1.0" @@ -21,17 +21,25 @@ android { } buildTypes { + debug { + resValue("string", "connect_instance_id", localProperties['connect.instanceId'] ?: "") + resValue("string", "contact_flow_id", localProperties['connect.contactFlowId'] ?: "") + resValue("string", "start_webrtc_endpoint", localProperties['endpoints.startWebRTC'] ?: "") + resValue("string", "create_participant_connection_endpoint", localProperties['endpoints.createParticipant'] ?: "") + resValue("string", "send_message_endpoint", localProperties['endpoints.sendMessage'] ?: "") + } + release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = '1.8' + jvmTarget = '17' } buildFeatures { viewBinding true @@ -40,15 +48,15 @@ android { } dependencies { - implementation 'androidx.core:core-ktx:1.7.0' - implementation 'androidx.appcompat:appcompat:1.6.1' - implementation 'com.google.android.material:material:1.9.0' - implementation 'androidx.constraintlayout:constraintlayout:2.1.4' - implementation 'androidx.navigation:navigation-fragment-ktx:2.6.0' - implementation 'androidx.navigation:navigation-ui-ktx:2.6.0' - implementation 'software.aws.chimesdk:amazon-chime-sdk:0.21.0' + implementation 'androidx.core:core-ktx:1.16.0' + implementation 'androidx.appcompat:appcompat:1.7.1' + implementation 'com.google.android.material:material:1.12.0' + implementation 'androidx.constraintlayout:constraintlayout:2.2.1' + implementation 'androidx.navigation:navigation-fragment-ktx:2.9.0' + implementation 'androidx.navigation:navigation-ui-ktx:2.9.0' + implementation 'software.aws.chimesdk:amazon-chime-sdk:0.25.0' implementation 'com.google.code.gson:gson:2.10.1' testImplementation 'junit:junit:4.13.2' - androidTestImplementation 'androidx.test.ext:junit:1.1.5' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' + androidTestImplementation 'androidx.test.ext:junit:1.2.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1' } diff --git a/Android/AmazonConnectInAppCallingAndroidSample/app/src/main/AndroidManifest.xml b/Android/AmazonConnectInAppCallingAndroidSample/app/src/main/AndroidManifest.xml index 8e66db1..2ff4099 100644 --- a/Android/AmazonConnectInAppCallingAndroidSample/app/src/main/AndroidManifest.xml +++ b/Android/AmazonConnectInAppCallingAndroidSample/app/src/main/AndroidManifest.xml @@ -10,6 +10,7 @@ + ) diff --git a/Android/AmazonConnectInAppCallingAndroidSample/app/src/main/java/com/amazonaws/services/connect/inappcalling/sample/MainActivity.kt b/Android/AmazonConnectInAppCallingAndroidSample/app/src/main/java/com/amazonaws/services/connect/inappcalling/sample/MainActivity.kt index f6da3cc..8fb7700 100644 --- a/Android/AmazonConnectInAppCallingAndroidSample/app/src/main/java/com/amazonaws/services/connect/inappcalling/sample/MainActivity.kt +++ b/Android/AmazonConnectInAppCallingAndroidSample/app/src/main/java/com/amazonaws/services/connect/inappcalling/sample/MainActivity.kt @@ -7,21 +7,35 @@ package com.amazonaws.services.connect.inappcalling.sample import android.os.Bundle import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.WindowCompat import com.amazonaws.services.connect.inappcalling.sample.common.showGeneralErrorAlert +import com.amazonaws.services.connect.inappcalling.sample.data.ConfigRepository import com.amazonaws.services.connect.inappcalling.sample.databinding.ActivityMainBinding import com.amazonaws.services.connect.inappcalling.sample.ui.CallSheet +import com.amazonaws.services.connect.inappcalling.sample.ui.utils.ConfigDialog const val displayNameKey = "DisplayName" const val cityKey = "City" class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding + private lateinit var configRepo: ConfigRepository + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + // set system bar's text color suits for a light bg + WindowCompat.getInsetsController(window, window.decorView) + .isAppearanceLightStatusBars = true binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) + configRepo = ConfigRepository(applicationContext) + + binding.btnConfig.setOnClickListener { + ConfigDialog(this, configRepo).show() + } + binding.startCallButton.setOnClickListener { startCall() } @@ -36,9 +50,21 @@ class MainActivity : AppCompatActivity() { showGeneralErrorAlert(getString(R.string.error_input_are_required)) return } + + val connectInstanceId = configRepo.getConnectInstanceId() + val contactFlowId = configRepo.getContactFlowId() + val startWebrtcEndpoint = configRepo.getStartWebrtcEndpoint() + val createParticipantConnectionEndpoint = configRepo.getCreateParticipantConnectionEndpoint() + val sendMessageEndpoint = configRepo.getSendMessageEndpoint() + // Create service locator for data access val config = CallConfiguration( applicationContext = applicationContext, + connectInstanceId = connectInstanceId, + contactFlowId = contactFlowId, + startWebrtcEndpoint = startWebrtcEndpoint, + createParticipantConnectionEndpoint = createParticipantConnectionEndpoint, + sendMessageEndpoint = sendMessageEndpoint, displayName = displayName, attributes = mapOf(displayNameKey to displayName, cityKey to city) ) diff --git a/Android/AmazonConnectInAppCallingAndroidSample/app/src/main/java/com/amazonaws/services/connect/inappcalling/sample/data/ConfigRepository.kt b/Android/AmazonConnectInAppCallingAndroidSample/app/src/main/java/com/amazonaws/services/connect/inappcalling/sample/data/ConfigRepository.kt new file mode 100644 index 0000000..05bd03a --- /dev/null +++ b/Android/AmazonConnectInAppCallingAndroidSample/app/src/main/java/com/amazonaws/services/connect/inappcalling/sample/data/ConfigRepository.kt @@ -0,0 +1,58 @@ +package com.amazonaws.services.connect.inappcalling.sample.data + +import android.content.Context +import androidx.core.content.edit +import com.amazonaws.services.connect.inappcalling.sample.R + +class ConfigRepository(private val context: Context) { + + private val prefs = context.getSharedPreferences("config_prefs", Context.MODE_PRIVATE) + + fun getConnectInstanceId(): String = + prefs.getString(KEY_CONNECT_INSTANCE_ID, null) + ?: context.getString(R.string.connect_instance_id) + + fun getContactFlowId(): String = + prefs.getString(KEY_CONTACT_FLOW_ID, null) + ?: context.getString(R.string.contact_flow_id) + + fun getStartWebrtcEndpoint(): String = + prefs.getString(KEY_START_WEBRTC_ENDPOINT, null) + ?: context.getString(R.string.start_webrtc_endpoint) + + fun getCreateParticipantConnectionEndpoint(): String = + prefs.getString(KEY_CREATE_PARTICIPANT_ENDPOINT, null) + ?: context.getString(R.string.create_participant_connection_endpoint) + + fun getSendMessageEndpoint(): String = + prefs.getString(KEY_SEND_MESSAGE_ENDPOINT, null) + ?: context.getString(R.string.send_message_endpoint) + + fun saveConnectInstanceId(value: String) { + prefs.edit { putString(KEY_CONNECT_INSTANCE_ID, value) } + } + + fun saveContactFlowId(value: String) { + prefs.edit { putString(KEY_CONTACT_FLOW_ID, value) } + } + + fun saveStartWebrtcEndpoint(value: String) { + prefs.edit { putString(KEY_START_WEBRTC_ENDPOINT, value) } + } + + fun saveCreateParticipantConnectionEndpoint(value: String) { + prefs.edit { putString(KEY_CREATE_PARTICIPANT_ENDPOINT, value) } + } + + fun saveSendMessageEndpoint(value: String) { + prefs.edit { putString(KEY_SEND_MESSAGE_ENDPOINT, value) } + } + + companion object { + private const val KEY_CONNECT_INSTANCE_ID = "connect_instance_id" + private const val KEY_CONTACT_FLOW_ID = "contact_flow_id" + private const val KEY_START_WEBRTC_ENDPOINT = "start_webrtc_endpoint" + private const val KEY_CREATE_PARTICIPANT_ENDPOINT = "create_participant_connection_endpoint" + private const val KEY_SEND_MESSAGE_ENDPOINT = "send_message_endpoint" + } +} \ No newline at end of file diff --git a/Android/AmazonConnectInAppCallingAndroidSample/app/src/main/java/com/amazonaws/services/connect/inappcalling/sample/data/domain/CallManager.kt b/Android/AmazonConnectInAppCallingAndroidSample/app/src/main/java/com/amazonaws/services/connect/inappcalling/sample/data/domain/CallManager.kt index 9becdd0..4487784 100644 --- a/Android/AmazonConnectInAppCallingAndroidSample/app/src/main/java/com/amazonaws/services/connect/inappcalling/sample/data/domain/CallManager.kt +++ b/Android/AmazonConnectInAppCallingAndroidSample/app/src/main/java/com/amazonaws/services/connect/inappcalling/sample/data/domain/CallManager.kt @@ -9,10 +9,12 @@ import android.Manifest import android.content.ComponentName import android.content.Context import android.content.Intent +import android.content.ServiceConnection import android.content.pm.PackageManager import android.net.Uri import android.os.Build import android.os.Bundle +import android.os.IBinder import android.telecom.DisconnectCause import android.telecom.PhoneAccount import android.telecom.PhoneAccountHandle @@ -56,6 +58,7 @@ import com.amazonaws.services.connect.inappcalling.sample.data.domain.screenshar import com.amazonaws.services.connect.inappcalling.sample.data.utils.ConnectionTokenProvider import com.amazonaws.services.connect.inappcalling.sample.data.utils.onSuccess import com.amazonaws.services.connect.inappcalling.sample.service.CallConnectionService +import com.amazonaws.services.connect.inappcalling.sample.service.MicrophoneService class CallManager( private val config: CallConfiguration, @@ -78,6 +81,16 @@ class CallManager( private var meetingSession: MeetingSession? = null private lateinit var cameraCaptureSource: CameraCaptureSource private lateinit var backgroundBlurVideoFrameProcessor: BackgroundBlurVideoFrameProcessor + private var isMicrophoneServiceBound = false + private var microphoneServiceConnection = object : ServiceConnection { + override fun onServiceConnected(name: ComponentName?, service: IBinder?) { + isMicrophoneServiceBound = true + } + + override fun onServiceDisconnected(name: ComponentName?) { + isMicrophoneServiceBound = false + } + } init { registerPhoneAccount() @@ -103,6 +116,7 @@ class CallManager( == PackageManager.PERMISSION_GRANTED) { telecomManager.placeCall(Uri.parse("sip:dummy"), extras) } + startMicrophoneService() // Initialize call data callStateRepository.setLocalAttendeeId(attendeeId = response.connectionData.attendee.attendeeId) @@ -188,6 +202,11 @@ class CallManager( // Disconnect telecom connection CallConnectionService.connection?.setDisconnected(DisconnectCause(DisconnectCause.LOCAL)) CallConnectionService.connection?.destroy() + // Disconnect microphone service + if (isMicrophoneServiceBound) { + config.applicationContext.unbindService(microphoneServiceConnection) + isMicrophoneServiceBound = false + } callStateRepository.updateCallState(newState = CallState.NOT_STARTED) } @@ -418,6 +437,10 @@ class CallManager( ) } + override fun onDataMessageReceived(dataMessage: DataMessage) { + screenShareManager.handleDataMessage(dataMessage) + } + private fun notifyCallerMuteStateChange(state: CallerMuteState) { callStateRepository.updateCallerMuteState(state) } @@ -447,7 +470,7 @@ class CallManager( /** - * Connection Service integration + * Service integration */ private fun registerPhoneAccount() { @@ -472,8 +495,21 @@ class CallManager( } } - override fun onDataMessageReceived(dataMessage: DataMessage) { - screenShareManager.handleDataMessage(dataMessage) + private fun startMicrophoneService() { + // Start microphone service + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + config.applicationContext.startForegroundService( + Intent( + config.applicationContext, + MicrophoneService::class.java + ).also { intent -> + config.applicationContext.bindService( + intent, + microphoneServiceConnection, + Context.BIND_AUTO_CREATE + ) + } + ) + } } - } diff --git a/Android/AmazonConnectInAppCallingAndroidSample/app/src/main/java/com/amazonaws/services/connect/inappcalling/sample/data/domain/screenshare/ScreenCaptureService.kt b/Android/AmazonConnectInAppCallingAndroidSample/app/src/main/java/com/amazonaws/services/connect/inappcalling/sample/data/domain/screenshare/ScreenCaptureService.kt index c00b7ba..4f2a4e7 100644 --- a/Android/AmazonConnectInAppCallingAndroidSample/app/src/main/java/com/amazonaws/services/connect/inappcalling/sample/data/domain/screenshare/ScreenCaptureService.kt +++ b/Android/AmazonConnectInAppCallingAndroidSample/app/src/main/java/com/amazonaws/services/connect/inappcalling/sample/data/domain/screenshare/ScreenCaptureService.kt @@ -5,6 +5,7 @@ import android.app.NotificationManager import android.app.Service import android.content.Context import android.content.Intent +import android.content.pm.ServiceInfo import android.os.Binder import android.os.Build import android.os.IBinder @@ -16,7 +17,7 @@ class ScreenCaptureService : Service() { private val CHANNEL_ID = "ScreenCaptureServiceChannelID" private val CHANNEL_NAME = "Screen Share" - private val SERVICE_ID = 1 + private val SERVICE_ID = 1002 private val binder = ScreenCaptureBinder() @@ -41,16 +42,29 @@ class ScreenCaptureService : Service() { notificationManager.createNotificationChannel(channel) } - startForeground( - SERVICE_ID, - NotificationCompat.Builder(this, CHANNEL_ID) - .setSmallIcon(R.mipmap.ic_launcher) - .setContentTitle(getString(R.string.screen_capture_notification_tile)) - .setContentText(getText(R.string.screen_capture_notification_text)) - .setPriority(NotificationCompat.PRIORITY_DEFAULT) - .build() - ) - return Service.START_STICKY + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + startForeground( + SERVICE_ID, + NotificationCompat.Builder(this, CHANNEL_ID) + .setSmallIcon(R.mipmap.ic_launcher) + .setContentTitle(getString(R.string.screen_capture_notification_tile)) + .setContentText(getText(R.string.screen_capture_notification_text)) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .build(), + ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION + ) + } else { + startForeground( + SERVICE_ID, + NotificationCompat.Builder(this, CHANNEL_ID) + .setSmallIcon(R.mipmap.ic_launcher) + .setContentTitle(getString(R.string.screen_capture_notification_tile)) + .setContentText(getText(R.string.screen_capture_notification_text)) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .build() + ) + } + return START_STICKY } override fun onBind(intent: Intent?): IBinder? = binder diff --git a/Android/AmazonConnectInAppCallingAndroidSample/app/src/main/java/com/amazonaws/services/connect/inappcalling/sample/data/domain/screenshare/ScreenShareManager.kt b/Android/AmazonConnectInAppCallingAndroidSample/app/src/main/java/com/amazonaws/services/connect/inappcalling/sample/data/domain/screenshare/ScreenShareManager.kt index 1f1022f..564ec38 100644 --- a/Android/AmazonConnectInAppCallingAndroidSample/app/src/main/java/com/amazonaws/services/connect/inappcalling/sample/data/domain/screenshare/ScreenShareManager.kt +++ b/Android/AmazonConnectInAppCallingAndroidSample/app/src/main/java/com/amazonaws/services/connect/inappcalling/sample/data/domain/screenshare/ScreenShareManager.kt @@ -36,10 +36,10 @@ class ScreenShareManager( private val logger = ConsoleLogger() private var isBound: Boolean = false - /* - * Check if the data message is from topic `AMAZON_CONNECT_SCREEN_SHARING`, - * if yes, it will update ScreenShareCapabilityEnabled flag accordingly, - * if no, it will return immediately. + /** + * Handle data messages sent on the AMAZON_CONNECT_SCREEN_SHARING topic. + * STARTED -> enable screen share + * STOPPED -> stop screen share */ fun handleDataMessage(dataMessage: DataMessage) { if(dataMessage.topic != AMAZON_CONNECT_SCREEN_SHARING_TOPIC) { @@ -63,6 +63,9 @@ class ScreenShareManager( } } + /** + * Start screen share using Chime SDK screen capture. + */ fun startScreenShare(resultCode: Int, data: Intent, screenShareController: ContentShareController ) { @@ -76,7 +79,6 @@ class ScreenShareManager( val screenCaptureSourceObserver = object : CaptureSourceObserver { override fun onCaptureStarted() { videoSource?.let { screenShareController.startContentShare(this@ScreenShareManager) } - // callStateRepository.updateScreenShareTileStates(null) callStateRepository.updateScreenShareStatus(ScreenShareStatus.LOCAL) } @@ -84,6 +86,9 @@ class ScreenShareManager( if(callStateRepository.getScreenShareStatus() != ScreenShareStatus.REMOTE) { callStateRepository.updateScreenShareStatus(ScreenShareStatus.NONE) } + // When capturing gets turned off from system UI, + // we want to stop sending to remote as well + screenShareController.stopContentShare() } override fun onCaptureFailed(error: CaptureSourceError) { @@ -124,7 +129,6 @@ class ScreenShareManager( screenCaptureSource?.stop() screenCaptureSource?.release() - // screenCaptureSource?.release() screenCaptureConnectionService?.let { if (isBound) context.unbindService(it) } @@ -132,11 +136,11 @@ class ScreenShareManager( } fun addVideoSink(videoSink: VideoSink) { - screenCaptureSource?.let { it.addVideoSink(videoSink) } + screenCaptureSource?.addVideoSink(videoSink) } fun removeVideoSink(videoSink: VideoSink) { - screenCaptureSource?.let { it.removeVideoSink(videoSink) } + screenCaptureSource?.removeVideoSink(videoSink) } fun addObserver(observer: CaptureSourceObserver) = screenCaptureSource?.addCaptureSourceObserver(observer) diff --git a/Android/AmazonConnectInAppCallingAndroidSample/app/src/main/java/com/amazonaws/services/connect/inappcalling/sample/service/MicrophoneService.kt b/Android/AmazonConnectInAppCallingAndroidSample/app/src/main/java/com/amazonaws/services/connect/inappcalling/sample/service/MicrophoneService.kt new file mode 100644 index 0000000..8574387 --- /dev/null +++ b/Android/AmazonConnectInAppCallingAndroidSample/app/src/main/java/com/amazonaws/services/connect/inappcalling/sample/service/MicrophoneService.kt @@ -0,0 +1,70 @@ +package com.amazonaws.services.connect.inappcalling.sample.service + +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.Service +import android.content.Context +import android.content.Intent +import android.content.pm.ServiceInfo +import android.os.Binder +import android.os.Build +import android.os.IBinder +import androidx.core.app.NotificationCompat +import com.amazonaws.services.connect.inappcalling.sample.R + +class MicrophoneService: Service() { + private lateinit var notificationManager: NotificationManager + private val CHANNEL_ID = "MicrophoneServiceChannelID" + private val CHANNEL_NAME = "Microphone" + private val NOTIFICATION_ID = 1001 + + private val binder = MicrophoneBinder() + + inner class MicrophoneBinder : Binder() { + fun getService(): MicrophoneService = this@MicrophoneService + } + + override fun onCreate() { + super.onCreate() + notificationManager = + applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val channel = NotificationChannel( + CHANNEL_ID, + CHANNEL_NAME, + NotificationManager.IMPORTANCE_DEFAULT + ) + notificationManager.createNotificationChannel(channel) + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + startForeground( + NOTIFICATION_ID, + NotificationCompat.Builder(this, CHANNEL_ID) + .setSmallIcon(R.mipmap.ic_launcher) + .setContentTitle(getString(R.string.microphone_notification_tile)) + .setContentText(getText(R.string.microphone_notification_text)) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .build(), + ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE + ) + } else { + startForeground( + NOTIFICATION_ID, + NotificationCompat.Builder(this, CHANNEL_ID) + .setSmallIcon(R.mipmap.ic_launcher) + .setContentTitle(getString(R.string.microphone_notification_tile)) + .setContentText(getText(R.string.microphone_notification_text)) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .build() + ) + } + + return START_STICKY + } + + override fun onBind(intent: Intent?): IBinder = binder +} \ No newline at end of file diff --git a/Android/AmazonConnectInAppCallingAndroidSample/app/src/main/java/com/amazonaws/services/connect/inappcalling/sample/ui/utils/ConfigDialog.kt b/Android/AmazonConnectInAppCallingAndroidSample/app/src/main/java/com/amazonaws/services/connect/inappcalling/sample/ui/utils/ConfigDialog.kt new file mode 100644 index 0000000..f060592 --- /dev/null +++ b/Android/AmazonConnectInAppCallingAndroidSample/app/src/main/java/com/amazonaws/services/connect/inappcalling/sample/ui/utils/ConfigDialog.kt @@ -0,0 +1,83 @@ +package com.amazonaws.services.connect.inappcalling.sample.ui.utils + +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity +import com.amazonaws.services.connect.inappcalling.sample.R +import com.amazonaws.services.connect.inappcalling.sample.data.ConfigRepository +import com.amazonaws.services.connect.inappcalling.sample.databinding.ConfigDialogBinding +import com.google.android.material.dialog.MaterialAlertDialogBuilder + +class ConfigDialog( + private val activity: AppCompatActivity, + private val configRepository: ConfigRepository +) { + + /** + * Show the configuration dialog. + * Loads current config values into inputs, + * sets up buttons for Save, Cancel and Reset. + */ + fun show() { + val binding = ConfigDialogBinding.inflate(activity.layoutInflater) + loadValues(binding) + + MaterialAlertDialogBuilder(activity) + .setTitle("Configure Endpoints") + .setView(binding.root) + .setPositiveButton("Save") { _, _ -> + saveFromBinding(binding) + Toast.makeText(activity, "Saved", Toast.LENGTH_SHORT).show() + } + .setNegativeButton("Cancel", null) + .setNeutralButton("Reset") { _, _ -> + resetToDefault(binding) + saveFromBinding(binding) + Toast.makeText(activity, "Reset to default and saved", Toast.LENGTH_SHORT).show() + } + .show() + } + + /** + * Load current stored configuration values into the input fields. + */ + private fun loadValues(binding: ConfigDialogBinding) { + binding.etConnectInstanceId.setText(configRepository.getConnectInstanceId()) + binding.etContactFlowId.setText(configRepository.getContactFlowId()) + binding.etStartWebrtcEndpoint.setText(configRepository.getStartWebrtcEndpoint()) + binding.etCreateParticipantConnectionEndpoint.setText(configRepository.getCreateParticipantConnectionEndpoint()) + binding.etSendMessageEndpoint.setText(configRepository.getSendMessageEndpoint()) + } + + /** + * Save the input values from dialog into the repository. + * Only saves non-empty values. + */ + private fun saveFromBinding(binding: ConfigDialogBinding) { + binding.etConnectInstanceId.text.toString().trim().takeIf { it.isNotEmpty() }?.let { + configRepository.saveConnectInstanceId(it) + } + binding.etContactFlowId.text.toString().trim().takeIf { it.isNotEmpty() }?.let { + configRepository.saveContactFlowId(it) + } + binding.etStartWebrtcEndpoint.text.toString().trim().takeIf { it.isNotEmpty() }?.let { + configRepository.saveStartWebrtcEndpoint(it) + } + binding.etCreateParticipantConnectionEndpoint.text.toString().trim().takeIf { it.isNotEmpty() }?.let { + configRepository.saveCreateParticipantConnectionEndpoint(it) + } + binding.etSendMessageEndpoint.text.toString().trim().takeIf { it.isNotEmpty() }?.let { + configRepository.saveSendMessageEndpoint(it) + } + } + + /** + * Reset the input fields to default values stored in string resources. + */ + private fun resetToDefault(binding: ConfigDialogBinding) { + binding.etConnectInstanceId.setText(activity.getString(R.string.connect_instance_id)) + binding.etContactFlowId.setText(activity.getString(R.string.contact_flow_id)) + binding.etStartWebrtcEndpoint.setText(activity.getString(R.string.start_webrtc_endpoint)) + binding.etCreateParticipantConnectionEndpoint.setText(activity.getString(R.string.create_participant_connection_endpoint)) + binding.etSendMessageEndpoint.setText(activity.getString(R.string.send_message_endpoint)) + } +} \ No newline at end of file diff --git a/Android/AmazonConnectInAppCallingAndroidSample/app/src/main/res/layout/activity_main.xml b/Android/AmazonConnectInAppCallingAndroidSample/app/src/main/res/layout/activity_main.xml index 8bf468e..153411a 100644 --- a/Android/AmazonConnectInAppCallingAndroidSample/app/src/main/res/layout/activity_main.xml +++ b/Android/AmazonConnectInAppCallingAndroidSample/app/src/main/res/layout/activity_main.xml @@ -69,15 +69,30 @@ android:layout_height="wrap_content" /> + +